@arcaelas/dynamite 1.0.18 → 1.0.23
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/package.json +11 -42
- package/src/scripts/load_seed.d.ts +5 -0
- package/src/scripts/load_seed.js +54 -0
- package/src/src/@types/index.d.ts +188 -0
- package/src/src/@types/index.js +9 -0
- package/src/{core → src/core}/client.d.ts +4 -16
- package/src/{core → src/core}/client.js +115 -38
- package/src/{core → src/core}/decorator.d.ts +0 -15
- package/src/{core → src/core}/decorator.js +5 -35
- package/src/src/core/table.d.ts +81 -0
- package/src/src/core/table.js +892 -0
- package/src/{decorators → src/decorators}/indexes.d.ts +1 -1
- package/src/{decorators → src/decorators}/indexes.js +12 -20
- package/src/{decorators → src/decorators}/relations.d.ts +20 -1
- package/src/{decorators → src/decorators}/relations.js +32 -2
- package/src/{decorators → src/decorators}/timestamps.d.ts +4 -4
- package/src/{decorators → src/decorators}/timestamps.js +11 -6
- package/src/{decorators → src/decorators}/transforms.d.ts +17 -4
- package/src/{decorators → src/decorators}/transforms.js +40 -28
- package/src/{index.d.ts → src/index.d.ts} +4 -4
- package/src/{index.js → src/index.js} +3 -2
- package/src/src/index.test.d.ts +6 -0
- package/src/src/index.test.js +789 -0
- package/src/{utils → src/utils}/relations.d.ts +5 -3
- package/src/src/utils/relations.js +216 -0
- package/src/@types/index.d.ts +0 -102
- package/src/@types/index.js +0 -9
- package/src/core/method.d.ts +0 -73
- package/src/core/method.js +0 -140
- package/src/core/table.d.ts +0 -56
- package/src/core/table.js +0 -659
- package/src/index.test.d.ts +0 -13
- package/src/index.test.js +0 -1992
- package/src/utils/relations.js +0 -141
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file index.test.ts
|
|
4
|
+
* @description Test suite para Dynamite ORM
|
|
5
|
+
* @run tsx --tsconfig tsx.config.json src/index.test.ts
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
24
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
25
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
26
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
27
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
28
|
+
};
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
47
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
48
|
+
};
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
const client_1 = require("./core/client");
|
|
54
|
+
const table_1 = __importDefault(require("./core/table"));
|
|
55
|
+
const indexes_1 = require("./decorators/indexes");
|
|
56
|
+
const relations_1 = require("./decorators/relations");
|
|
57
|
+
const timestamps_1 = require("./decorators/timestamps");
|
|
58
|
+
const transforms_1 = require("./decorators/transforms");
|
|
59
|
+
function assert(condition, message) {
|
|
60
|
+
if (!condition)
|
|
61
|
+
throw new Error(message);
|
|
62
|
+
}
|
|
63
|
+
async function assert_throws(fn, expected_message) {
|
|
64
|
+
try {
|
|
65
|
+
await fn();
|
|
66
|
+
throw new Error("Expected function to throw");
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (expected_message && !error.message.includes(expected_message)) {
|
|
70
|
+
throw new Error(`Expected "${expected_message}" but got "${error.message}"`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// MODELS
|
|
76
|
+
// =============================================================================
|
|
77
|
+
let User = class User extends table_1.default {
|
|
78
|
+
};
|
|
79
|
+
__decorate([
|
|
80
|
+
(0, indexes_1.PrimaryKey)(),
|
|
81
|
+
(0, transforms_1.Default)(() => `user-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
82
|
+
__metadata("design:type", String)
|
|
83
|
+
], User.prototype, "id", void 0);
|
|
84
|
+
__decorate([
|
|
85
|
+
(0, transforms_1.NotNull)("Name required"),
|
|
86
|
+
(0, transforms_1.Validate)((v) => (typeof v === "string" && v.length >= 3) || "Name min 3 chars"),
|
|
87
|
+
__metadata("design:type", String)
|
|
88
|
+
], User.prototype, "name", void 0);
|
|
89
|
+
__decorate([
|
|
90
|
+
(0, transforms_1.Validate)((v) => (typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) || "Invalid email"),
|
|
91
|
+
__metadata("design:type", String)
|
|
92
|
+
], User.prototype, "email", void 0);
|
|
93
|
+
__decorate([
|
|
94
|
+
(0, transforms_1.Default)(18),
|
|
95
|
+
(0, transforms_1.Mutate)((v) => (typeof v === "string" ? parseInt(v, 10) : v)),
|
|
96
|
+
__metadata("design:type", Number)
|
|
97
|
+
], User.prototype, "age", void 0);
|
|
98
|
+
__decorate([
|
|
99
|
+
(0, relations_1.HasMany)(() => Order, "user_id", "id"),
|
|
100
|
+
__metadata("design:type", Object)
|
|
101
|
+
], User.prototype, "orders", void 0);
|
|
102
|
+
__decorate([
|
|
103
|
+
(0, relations_1.HasOne)(() => Profile, "user_id", "id"),
|
|
104
|
+
__metadata("design:type", Object)
|
|
105
|
+
], User.prototype, "profile", void 0);
|
|
106
|
+
__decorate([
|
|
107
|
+
(0, relations_1.ManyToMany)(() => Role, "test_users_roles", "user_id", "role_id"),
|
|
108
|
+
__metadata("design:type", Object)
|
|
109
|
+
], User.prototype, "roles", void 0);
|
|
110
|
+
__decorate([
|
|
111
|
+
(0, timestamps_1.CreatedAt)(),
|
|
112
|
+
__metadata("design:type", Object)
|
|
113
|
+
], User.prototype, "created_at", void 0);
|
|
114
|
+
__decorate([
|
|
115
|
+
(0, timestamps_1.UpdatedAt)(),
|
|
116
|
+
__metadata("design:type", Object)
|
|
117
|
+
], User.prototype, "updated_at", void 0);
|
|
118
|
+
User = __decorate([
|
|
119
|
+
(0, transforms_1.Name)("test_users")
|
|
120
|
+
], User);
|
|
121
|
+
let Product = class Product extends table_1.default {
|
|
122
|
+
};
|
|
123
|
+
__decorate([
|
|
124
|
+
(0, indexes_1.PrimaryKey)(),
|
|
125
|
+
(0, transforms_1.Default)(() => `prod-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
126
|
+
__metadata("design:type", String)
|
|
127
|
+
], Product.prototype, "id", void 0);
|
|
128
|
+
__decorate([
|
|
129
|
+
(0, transforms_1.NotNull)("Name required"),
|
|
130
|
+
__metadata("design:type", String)
|
|
131
|
+
], Product.prototype, "name", void 0);
|
|
132
|
+
__decorate([
|
|
133
|
+
(0, transforms_1.Column)(),
|
|
134
|
+
__metadata("design:type", Number)
|
|
135
|
+
], Product.prototype, "price", void 0);
|
|
136
|
+
__decorate([
|
|
137
|
+
(0, transforms_1.Column)(),
|
|
138
|
+
__metadata("design:type", String)
|
|
139
|
+
], Product.prototype, "category_id", void 0);
|
|
140
|
+
__decorate([
|
|
141
|
+
(0, transforms_1.Column)(),
|
|
142
|
+
__metadata("design:type", String)
|
|
143
|
+
], Product.prototype, "owner_id", void 0);
|
|
144
|
+
__decorate([
|
|
145
|
+
(0, relations_1.BelongsTo)(() => Category, "category_id", "id"),
|
|
146
|
+
__metadata("design:type", Object)
|
|
147
|
+
], Product.prototype, "category", void 0);
|
|
148
|
+
__decorate([
|
|
149
|
+
(0, relations_1.BelongsTo)(() => User, "owner_id", "id"),
|
|
150
|
+
__metadata("design:type", Object)
|
|
151
|
+
], Product.prototype, "owner", void 0);
|
|
152
|
+
Product = __decorate([
|
|
153
|
+
(0, transforms_1.Name)("test_products")
|
|
154
|
+
], Product);
|
|
155
|
+
let Order = class Order extends table_1.default {
|
|
156
|
+
};
|
|
157
|
+
__decorate([
|
|
158
|
+
(0, indexes_1.PrimaryKey)(),
|
|
159
|
+
(0, transforms_1.Default)(() => `order-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
160
|
+
__metadata("design:type", String)
|
|
161
|
+
], Order.prototype, "id", void 0);
|
|
162
|
+
__decorate([
|
|
163
|
+
(0, transforms_1.Column)(),
|
|
164
|
+
__metadata("design:type", String)
|
|
165
|
+
], Order.prototype, "user_id", void 0);
|
|
166
|
+
__decorate([
|
|
167
|
+
(0, transforms_1.Column)(),
|
|
168
|
+
__metadata("design:type", Number)
|
|
169
|
+
], Order.prototype, "total", void 0);
|
|
170
|
+
__decorate([
|
|
171
|
+
(0, transforms_1.Column)(),
|
|
172
|
+
__metadata("design:type", String)
|
|
173
|
+
], Order.prototype, "status", void 0);
|
|
174
|
+
__decorate([
|
|
175
|
+
(0, relations_1.BelongsTo)(() => User, "user_id", "id"),
|
|
176
|
+
__metadata("design:type", Object)
|
|
177
|
+
], Order.prototype, "user", void 0);
|
|
178
|
+
Order = __decorate([
|
|
179
|
+
(0, transforms_1.Name)("test_orders")
|
|
180
|
+
], Order);
|
|
181
|
+
let Category = class Category extends table_1.default {
|
|
182
|
+
};
|
|
183
|
+
__decorate([
|
|
184
|
+
(0, indexes_1.PrimaryKey)(),
|
|
185
|
+
(0, transforms_1.Default)(() => `cat-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
186
|
+
__metadata("design:type", String)
|
|
187
|
+
], Category.prototype, "id", void 0);
|
|
188
|
+
__decorate([
|
|
189
|
+
(0, transforms_1.Mutate)((v) => (typeof v === "string" ? v.toLowerCase() : v)),
|
|
190
|
+
__metadata("design:type", String)
|
|
191
|
+
], Category.prototype, "name", void 0);
|
|
192
|
+
__decorate([
|
|
193
|
+
(0, relations_1.HasMany)(() => Product, "category_id", "id"),
|
|
194
|
+
__metadata("design:type", Object)
|
|
195
|
+
], Category.prototype, "products", void 0);
|
|
196
|
+
Category = __decorate([
|
|
197
|
+
(0, transforms_1.Name)("test_categories")
|
|
198
|
+
], Category);
|
|
199
|
+
let Role = class Role extends table_1.default {
|
|
200
|
+
};
|
|
201
|
+
__decorate([
|
|
202
|
+
(0, indexes_1.PrimaryKey)(),
|
|
203
|
+
(0, transforms_1.Default)(() => `role-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
204
|
+
__metadata("design:type", String)
|
|
205
|
+
], Role.prototype, "id", void 0);
|
|
206
|
+
__decorate([
|
|
207
|
+
(0, relations_1.ManyToMany)(() => User, "test_users_roles", "role_id", "user_id"),
|
|
208
|
+
__metadata("design:type", Object)
|
|
209
|
+
], Role.prototype, "users", void 0);
|
|
210
|
+
Role = __decorate([
|
|
211
|
+
(0, transforms_1.Name)("test_roles")
|
|
212
|
+
], Role);
|
|
213
|
+
let SoftDeleteModel = class SoftDeleteModel extends table_1.default {
|
|
214
|
+
};
|
|
215
|
+
__decorate([
|
|
216
|
+
(0, indexes_1.PrimaryKey)(),
|
|
217
|
+
(0, transforms_1.Default)(() => `soft-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
218
|
+
__metadata("design:type", String)
|
|
219
|
+
], SoftDeleteModel.prototype, "id", void 0);
|
|
220
|
+
__decorate([
|
|
221
|
+
(0, timestamps_1.DeleteAt)(),
|
|
222
|
+
__metadata("design:type", String)
|
|
223
|
+
], SoftDeleteModel.prototype, "deleted_at", void 0);
|
|
224
|
+
SoftDeleteModel = __decorate([
|
|
225
|
+
(0, transforms_1.Name)("test_soft_delete")
|
|
226
|
+
], SoftDeleteModel);
|
|
227
|
+
let Profile = class Profile extends table_1.default {
|
|
228
|
+
};
|
|
229
|
+
__decorate([
|
|
230
|
+
(0, indexes_1.PrimaryKey)(),
|
|
231
|
+
(0, transforms_1.Default)(() => `profile-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
232
|
+
__metadata("design:type", String)
|
|
233
|
+
], Profile.prototype, "id", void 0);
|
|
234
|
+
__decorate([
|
|
235
|
+
(0, transforms_1.Column)(),
|
|
236
|
+
__metadata("design:type", String)
|
|
237
|
+
], Profile.prototype, "user_id", void 0);
|
|
238
|
+
__decorate([
|
|
239
|
+
(0, transforms_1.Column)(),
|
|
240
|
+
__metadata("design:type", String)
|
|
241
|
+
], Profile.prototype, "bio", void 0);
|
|
242
|
+
__decorate([
|
|
243
|
+
(0, relations_1.BelongsTo)(() => User, "user_id", "id"),
|
|
244
|
+
__metadata("design:type", Object)
|
|
245
|
+
], Profile.prototype, "user", void 0);
|
|
246
|
+
Profile = __decorate([
|
|
247
|
+
(0, transforms_1.Name)("test_profiles")
|
|
248
|
+
], Profile);
|
|
249
|
+
const isPositive = (v) => v > 0 || "Must be positive";
|
|
250
|
+
const isLessThan100 = (v) => v < 100 || "Must be < 100";
|
|
251
|
+
let ValidatorModel = class ValidatorModel extends table_1.default {
|
|
252
|
+
};
|
|
253
|
+
__decorate([
|
|
254
|
+
(0, indexes_1.PrimaryKey)(),
|
|
255
|
+
(0, transforms_1.Default)(() => `val-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
256
|
+
__metadata("design:type", String)
|
|
257
|
+
], ValidatorModel.prototype, "id", void 0);
|
|
258
|
+
__decorate([
|
|
259
|
+
(0, transforms_1.Default)(5),
|
|
260
|
+
(0, transforms_1.Validate)((v) => v >= 0 || "Must be >= 0"),
|
|
261
|
+
__metadata("design:type", Number)
|
|
262
|
+
], ValidatorModel.prototype, "with_default", void 0);
|
|
263
|
+
__decorate([
|
|
264
|
+
(0, transforms_1.Validate)((v) => v.length >= 3 || "Min 3 chars"),
|
|
265
|
+
(0, transforms_1.Mutate)((v) => v.trim().toLowerCase()),
|
|
266
|
+
__metadata("design:type", String)
|
|
267
|
+
], ValidatorModel.prototype, "transformed", void 0);
|
|
268
|
+
__decorate([
|
|
269
|
+
(0, transforms_1.Validate)((v) => v <= 1000 || "Max 1000"),
|
|
270
|
+
(0, transforms_1.Mutate)((v) => Math.abs(v)),
|
|
271
|
+
(0, transforms_1.NotNull)("Required"),
|
|
272
|
+
__metadata("design:type", Number)
|
|
273
|
+
], ValidatorModel.prototype, "absolute", void 0);
|
|
274
|
+
__decorate([
|
|
275
|
+
(0, transforms_1.Serialize)(JSON.parse, JSON.stringify),
|
|
276
|
+
__metadata("design:type", Object)
|
|
277
|
+
], ValidatorModel.prototype, "metadata", void 0);
|
|
278
|
+
__decorate([
|
|
279
|
+
(0, transforms_1.Validate)([isPositive, isLessThan100]),
|
|
280
|
+
__metadata("design:type", Number)
|
|
281
|
+
], ValidatorModel.prototype, "multi_validated", void 0);
|
|
282
|
+
__decorate([
|
|
283
|
+
(0, transforms_1.Name)("db_column_name"),
|
|
284
|
+
(0, transforms_1.Column)(),
|
|
285
|
+
__metadata("design:type", String)
|
|
286
|
+
], ValidatorModel.prototype, "renamed", void 0);
|
|
287
|
+
ValidatorModel = __decorate([
|
|
288
|
+
(0, transforms_1.Name)("test_validators")
|
|
289
|
+
], ValidatorModel);
|
|
290
|
+
(async function main() {
|
|
291
|
+
let passed = 0;
|
|
292
|
+
let failed = 0;
|
|
293
|
+
const failures = [];
|
|
294
|
+
const start = performance.now();
|
|
295
|
+
console.log("\n DYNAMITE ORM - TEST SUITE\n");
|
|
296
|
+
try {
|
|
297
|
+
const dynamite = new client_1.Dynamite({
|
|
298
|
+
endpoint: "http://localhost:8000",
|
|
299
|
+
region: "us-east-1",
|
|
300
|
+
credentials: { accessKeyId: "test", secretAccessKey: "test" },
|
|
301
|
+
tables: [User, Product, Order, Category, Role, SoftDeleteModel, Profile, ValidatorModel],
|
|
302
|
+
});
|
|
303
|
+
await dynamite.connect();
|
|
304
|
+
const { load_all } = await Promise.resolve().then(() => __importStar(require("../scripts/load_seed")));
|
|
305
|
+
await load_all();
|
|
306
|
+
const [categories, users, products, orders, roles] = await Promise.all([
|
|
307
|
+
Category.where({}, { limit: 5 }),
|
|
308
|
+
User.where({}, { limit: 10 }),
|
|
309
|
+
Product.where({}, { limit: 20 }),
|
|
310
|
+
Order.where({}, { limit: 15 }),
|
|
311
|
+
Role.where({}, { limit: 3 }),
|
|
312
|
+
]);
|
|
313
|
+
const tests = [
|
|
314
|
+
// =========================================================================
|
|
315
|
+
// DECORATORS
|
|
316
|
+
// =========================================================================
|
|
317
|
+
{
|
|
318
|
+
name: "@Default applies default value",
|
|
319
|
+
fn: async () => {
|
|
320
|
+
const user = new User({ name: "Test", email: "test@example.com" });
|
|
321
|
+
assert(user.age === 18, "Default age should be 18");
|
|
322
|
+
assert(typeof user.id === "string" && user.id.length > 0, "ID should be generated");
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "@Validate rejects invalid values",
|
|
327
|
+
fn: async () => {
|
|
328
|
+
await assert_throws(() => User.create({ name: "AB", email: "test@test.com" }), "Name min 3 chars");
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "@NotNull rejects null/undefined/empty",
|
|
333
|
+
fn: async () => {
|
|
334
|
+
await assert_throws(() => Product.create({ price: 100 }), "Name required");
|
|
335
|
+
await assert_throws(() => Product.create({ name: null, price: 100 }), "Name required");
|
|
336
|
+
await assert_throws(() => Product.create({ name: "", price: 100 }), "Name required");
|
|
337
|
+
await assert_throws(() => Product.create({ name: " ", price: 100 }), "Name required");
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "@Mutate transforms values",
|
|
342
|
+
fn: async () => {
|
|
343
|
+
const cat = new Category({ name: "UPPER" });
|
|
344
|
+
assert(cat.name === "upper", "Should transform to lowercase");
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "@CreatedAt sets timestamp on create",
|
|
349
|
+
fn: async () => {
|
|
350
|
+
const user = await User.create({ name: "New User", email: "new@test.com" });
|
|
351
|
+
assert(typeof user.created_at === "string", "created_at should be set");
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "@UpdatedAt sets timestamp on save",
|
|
356
|
+
fn: async () => {
|
|
357
|
+
const user = users[0];
|
|
358
|
+
const original = user.created_at;
|
|
359
|
+
user.name = "Updated";
|
|
360
|
+
await user.save();
|
|
361
|
+
assert(typeof user.updated_at === "string", "updated_at should be set");
|
|
362
|
+
assert(user.created_at === original, "created_at should not change");
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
// =========================================================================
|
|
366
|
+
// SOFT DELETE
|
|
367
|
+
// =========================================================================
|
|
368
|
+
{
|
|
369
|
+
name: "@DeleteAt marks record without physical deletion",
|
|
370
|
+
fn: async () => {
|
|
371
|
+
const record = await SoftDeleteModel.create({ name: "Test" });
|
|
372
|
+
assert(record.deleted_at === undefined, "deleted_at should be undefined initially");
|
|
373
|
+
await record.destroy();
|
|
374
|
+
const loaded = await SoftDeleteModel.withTrashed({ id: record.id });
|
|
375
|
+
assert(loaded.length === 1 && loaded[0].deleted_at !== undefined, "deleted_at should be set");
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "Normal queries exclude soft-deleted records",
|
|
380
|
+
fn: async () => {
|
|
381
|
+
const record = await SoftDeleteModel.create({ name: "ToDelete" });
|
|
382
|
+
await record.destroy();
|
|
383
|
+
const all = await SoftDeleteModel.where({});
|
|
384
|
+
assert(!all.some((r) => r.id === record.id), "Soft deleted should not appear");
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "onlyTrashed returns only deleted records",
|
|
389
|
+
fn: async () => {
|
|
390
|
+
const active = await SoftDeleteModel.create({ name: "Active" });
|
|
391
|
+
const deleted = await SoftDeleteModel.create({ name: "Deleted" });
|
|
392
|
+
await deleted.destroy();
|
|
393
|
+
const trashed = await SoftDeleteModel.onlyTrashed({});
|
|
394
|
+
assert(trashed.some((r) => r.id === deleted.id), "Should include deleted");
|
|
395
|
+
assert(!trashed.some((r) => r.id === active.id), "Should not include active");
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "forceDestroy permanently removes record",
|
|
400
|
+
fn: async () => {
|
|
401
|
+
const record = await SoftDeleteModel.create({ name: "ForceDelete" });
|
|
402
|
+
const id = record.id;
|
|
403
|
+
await record.forceDestroy();
|
|
404
|
+
const result = await SoftDeleteModel.withTrashed({ id });
|
|
405
|
+
assert(result.length === 0, "Record should not exist");
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
// =========================================================================
|
|
409
|
+
// RELATIONS - HasMany / BelongsTo
|
|
410
|
+
// =========================================================================
|
|
411
|
+
{
|
|
412
|
+
name: "@HasMany returns array of related records",
|
|
413
|
+
fn: async () => {
|
|
414
|
+
const order = orders[0];
|
|
415
|
+
const user = await User.first({ id: order.user_id }, { include: { orders: true } });
|
|
416
|
+
assert(user !== undefined, "User should exist");
|
|
417
|
+
assert(Array.isArray(user.orders) && user.orders.length > 0, "Should have orders");
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "@BelongsTo returns parent record",
|
|
422
|
+
fn: async () => {
|
|
423
|
+
const product = await Product.first({ id: products[0].id }, { include: { category: true } });
|
|
424
|
+
assert(product !== undefined, "Product should exist");
|
|
425
|
+
assert(product.category !== undefined && product.category.id === products[0].category_id, "Category should match");
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "@HasOne returns single object, not array",
|
|
430
|
+
fn: async () => {
|
|
431
|
+
const user = await User.create({ name: "HasOneUser", email: "hasone@test.com" });
|
|
432
|
+
const profile = await Profile.create({ user_id: user.id, bio: "Bio" });
|
|
433
|
+
const loaded = await User.first({ id: user.id }, { include: { profile: true } });
|
|
434
|
+
assert(loaded !== undefined && !Array.isArray(loaded.profile), "HasOne should return object");
|
|
435
|
+
assert(loaded.profile.id === profile.id, "Profile should match");
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "Empty HasMany returns empty array",
|
|
440
|
+
fn: async () => {
|
|
441
|
+
const user = await User.create({ name: "NoOrders", email: "noorders@test.com" });
|
|
442
|
+
const loaded = await User.first({ id: user.id }, { include: { orders: true } });
|
|
443
|
+
assert(loaded !== undefined && Array.isArray(loaded.orders) && loaded.orders.length === 0, "Should be empty array");
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "HasOne returns null when no related record",
|
|
448
|
+
fn: async () => {
|
|
449
|
+
const user = await User.create({ name: "NoProfile", email: "noprofile@test.com" });
|
|
450
|
+
const loaded = await User.first({ id: user.id }, { include: { profile: true } });
|
|
451
|
+
assert(loaded !== undefined && (loaded.profile === null || loaded.profile === undefined), "Profile should be null/undefined");
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "BelongsTo returns null when FK not found",
|
|
456
|
+
fn: async () => {
|
|
457
|
+
const order = await Order.create({ user_id: "non-existent", total: 100, status: "pending" });
|
|
458
|
+
const loaded = await Order.first({ id: order.id }, { include: { user: true } });
|
|
459
|
+
assert(loaded !== undefined && (loaded.user === null || loaded.user === undefined), "User should be null/undefined");
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
// =========================================================================
|
|
463
|
+
// RELATIONS - ManyToMany
|
|
464
|
+
// =========================================================================
|
|
465
|
+
{
|
|
466
|
+
name: "attach adds ManyToMany relation",
|
|
467
|
+
fn: async () => {
|
|
468
|
+
const user = users[1];
|
|
469
|
+
const role = roles[0];
|
|
470
|
+
await user.attach(Role, role.id);
|
|
471
|
+
const loaded = await User.first({ id: user.id }, { include: { roles: true } });
|
|
472
|
+
assert(loaded !== undefined && loaded.roles.some((r) => r.id === role.id), "Should have attached role");
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "detach removes ManyToMany relation",
|
|
477
|
+
fn: async () => {
|
|
478
|
+
const user = users[1];
|
|
479
|
+
const role = roles[0];
|
|
480
|
+
await user.detach(Role, role.id);
|
|
481
|
+
const loaded = await User.first({ id: user.id }, { include: { roles: true } });
|
|
482
|
+
assert(loaded !== undefined && !loaded.roles?.some((r) => r.id === role.id), "Role should be detached");
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "sync replaces ManyToMany relations",
|
|
487
|
+
fn: async () => {
|
|
488
|
+
const user = users[2];
|
|
489
|
+
await user.sync(Role, [roles[0].id, roles[1].id]);
|
|
490
|
+
const loaded = await User.first({ id: user.id }, { include: { roles: true } });
|
|
491
|
+
assert(loaded !== undefined && loaded.roles.length === 2, "Should have exactly 2 roles");
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
// =========================================================================
|
|
495
|
+
// QUERY OPTIONS
|
|
496
|
+
// =========================================================================
|
|
497
|
+
{
|
|
498
|
+
name: "limit returns N records",
|
|
499
|
+
fn: async () => {
|
|
500
|
+
const results = await User.where({}, { limit: 5 });
|
|
501
|
+
assert(results.length === 5, "Should return 5 records");
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "skip/offset paginates results",
|
|
506
|
+
fn: async () => {
|
|
507
|
+
const page1 = await User.where({}, { limit: 3, skip: 0 });
|
|
508
|
+
const page2 = await User.where({}, { limit: 3, skip: 3 });
|
|
509
|
+
assert(page1.length === 3 && page2.length === 3 && page1[0].id !== page2[0].id, "Pages should differ");
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "order sorts by field ASC/DESC",
|
|
514
|
+
fn: async () => {
|
|
515
|
+
const asc = await User.where({}, { order: { age: "ASC" }, limit: 5 });
|
|
516
|
+
const desc = await User.where({}, { order: { age: "DESC" }, limit: 5 });
|
|
517
|
+
for (let i = 1; i < asc.length; i++)
|
|
518
|
+
assert(asc[i - 1].age <= asc[i].age, "ASC order failed");
|
|
519
|
+
for (let i = 1; i < desc.length; i++)
|
|
520
|
+
assert(desc[i - 1].age >= desc[i].age, "DESC order failed");
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "attributes selects specific fields",
|
|
525
|
+
fn: async () => {
|
|
526
|
+
const [user] = await User.where({}, { attributes: ["id", "name"], limit: 1 });
|
|
527
|
+
assert(user.id !== undefined && user.name !== undefined && user.email === undefined, "Should only have id, name");
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "limit 0 returns empty array",
|
|
532
|
+
fn: async () => {
|
|
533
|
+
const results = await User.where({}, { limit: 0 });
|
|
534
|
+
assert(results.length === 0, "Should be empty");
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "offset > total returns empty array",
|
|
539
|
+
fn: async () => {
|
|
540
|
+
const results = await User.where({}, { offset: 100000 });
|
|
541
|
+
assert(results.length === 0, "Should be empty");
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
// =========================================================================
|
|
545
|
+
// WHERE OPERATORS
|
|
546
|
+
// =========================================================================
|
|
547
|
+
{
|
|
548
|
+
name: "where with equality",
|
|
549
|
+
fn: async () => {
|
|
550
|
+
const target = users[0].age;
|
|
551
|
+
const results = await User.where({ age: target });
|
|
552
|
+
assert(results.length > 0 && results.every((u) => u.age === target), "All should match age");
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "where with != operator",
|
|
557
|
+
fn: async () => {
|
|
558
|
+
const results = await User.where("age", "!=", 20);
|
|
559
|
+
assert(results.length > 0 && results.every((u) => u.age !== 20), "None should be 20");
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "where with > operator",
|
|
564
|
+
fn: async () => {
|
|
565
|
+
const results = await User.where("age", ">", 25);
|
|
566
|
+
assert(results.length > 0 && results.every((u) => u.age > 25), "All should be > 25");
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "where with < operator",
|
|
571
|
+
fn: async () => {
|
|
572
|
+
const results = await User.where("age", "<", 25);
|
|
573
|
+
assert(results.length > 0 && results.every((u) => u.age < 25), "All should be < 25");
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: "where with >= operator",
|
|
578
|
+
fn: async () => {
|
|
579
|
+
const results = await User.where("age", ">=", 25);
|
|
580
|
+
assert(results.length > 0 && results.every((u) => u.age >= 25), "All should be >= 25");
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: "where with <= operator",
|
|
585
|
+
fn: async () => {
|
|
586
|
+
const results = await User.where("age", "<=", 25);
|
|
587
|
+
assert(results.length > 0 && results.every((u) => u.age <= 25), "All should be <= 25");
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "where with in operator",
|
|
592
|
+
fn: async () => {
|
|
593
|
+
const ages = [20, 22, 24];
|
|
594
|
+
const results = await User.where("age", "in", ages);
|
|
595
|
+
assert(results.length > 0 && results.every((u) => ages.includes(u.age)), "All should be in list");
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "where with contains operator",
|
|
600
|
+
fn: async () => {
|
|
601
|
+
const results = await User.where({ name: { contains: "User" } });
|
|
602
|
+
assert(results.length > 0 && results.every((u) => u.name.includes("User")), "All should contain 'User'");
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: "where with range (between)",
|
|
607
|
+
fn: async () => {
|
|
608
|
+
const results = await User.where({ age: { $gt: 20, $lt: 30 } });
|
|
609
|
+
assert(results.length > 0 && results.every((u) => u.age > 20 && u.age < 30), "All should be between 20-30");
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "where with multiple conditions",
|
|
614
|
+
fn: async () => {
|
|
615
|
+
const results = await User.where({ age: { $gte: 20 }, name: { contains: "User" } });
|
|
616
|
+
results.forEach((u) => {
|
|
617
|
+
assert(u.age >= 20 && u.name.includes("User"), "Should match all conditions");
|
|
618
|
+
});
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
// =========================================================================
|
|
622
|
+
// INCLUDE OPTIONS
|
|
623
|
+
// =========================================================================
|
|
624
|
+
{
|
|
625
|
+
name: "include with where filters relations",
|
|
626
|
+
fn: async () => {
|
|
627
|
+
const order = orders[0];
|
|
628
|
+
order.status = "completed";
|
|
629
|
+
await order.save();
|
|
630
|
+
const user = await User.first({ id: order.user_id }, { include: { orders: { where: { status: "completed" } } } });
|
|
631
|
+
assert(user !== undefined && user.orders.every((o) => o.status === "completed"), "All orders should be completed");
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "include with limit paginates relations",
|
|
636
|
+
fn: async () => {
|
|
637
|
+
const loaded = await Category.first({ id: categories[0].id }, { include: { products: { limit: 2 } } });
|
|
638
|
+
assert(loaded !== undefined && loaded.products.length <= 2, "Should have <= 2 products");
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: "nested include loads 3 levels",
|
|
643
|
+
fn: async () => {
|
|
644
|
+
const loaded = await Category.first({ id: categories[0].id }, { include: { products: { include: { owner: { include: { orders: true } } } } } });
|
|
645
|
+
assert(loaded !== undefined && Array.isArray(loaded.products), "Products should be loaded");
|
|
646
|
+
if (loaded.products.length > 0 && loaded.products[0].owner) {
|
|
647
|
+
assert(Array.isArray(loaded.products[0].owner.orders), "3rd level should be loaded");
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "multiple includes load several relations",
|
|
653
|
+
fn: async () => {
|
|
654
|
+
const loaded = await Product.first({ id: products[0].id }, { include: { category: true, owner: true } });
|
|
655
|
+
assert(loaded !== undefined && loaded.category !== undefined && loaded.owner !== undefined, "Both relations loaded");
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
// =========================================================================
|
|
659
|
+
// VALIDATOR PIPELINES
|
|
660
|
+
// =========================================================================
|
|
661
|
+
{
|
|
662
|
+
name: "@Default + @Validate: default passes validation",
|
|
663
|
+
fn: async () => {
|
|
664
|
+
const model = new ValidatorModel({ transformed: "test", absolute: 5, metadata: {}, multi_validated: 50, renamed: "x" });
|
|
665
|
+
assert(model.with_default === 5, "Default should be applied");
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: "@Mutate → @Validate: transform then validate",
|
|
670
|
+
fn: async () => {
|
|
671
|
+
const model = new ValidatorModel({ transformed: " ABC ", absolute: 5, metadata: {}, multi_validated: 50, renamed: "x" });
|
|
672
|
+
assert(model.transformed === "abc", "Should trim and lowercase");
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: "@Mutate → @Validate: rejects invalid after transform",
|
|
677
|
+
fn: async () => {
|
|
678
|
+
await assert_throws(() => ValidatorModel.create({ transformed: " AB ", absolute: 5, metadata: {}, multi_validated: 50, renamed: "x" }), "Min 3 chars");
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
name: "@NotNull + @Mutate + @Validate: full pipeline",
|
|
683
|
+
fn: async () => {
|
|
684
|
+
const model = await ValidatorModel.create({ transformed: "test", absolute: -500, metadata: {}, multi_validated: 50, renamed: "x" });
|
|
685
|
+
assert(model.absolute === 500, "Should apply Math.abs");
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
name: "@NotNull rejects null in pipeline",
|
|
690
|
+
fn: async () => {
|
|
691
|
+
await assert_throws(() => ValidatorModel.create({ transformed: "test", absolute: null, metadata: {}, multi_validated: 50, renamed: "x" }), "Required");
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "@Validate array: all validators must pass",
|
|
696
|
+
fn: async () => {
|
|
697
|
+
await assert_throws(() => ValidatorModel.create({ transformed: "test", absolute: 5, metadata: {}, multi_validated: -5, renamed: "x" }), "Must be positive");
|
|
698
|
+
await assert_throws(() => ValidatorModel.create({ transformed: "test", absolute: 5, metadata: {}, multi_validated: 150, renamed: "x" }), "Must be < 100");
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: "@Name renames column in DB",
|
|
703
|
+
fn: async () => {
|
|
704
|
+
const model = await ValidatorModel.create({ transformed: "test", absolute: 5, metadata: {}, multi_validated: 50, renamed: "value" });
|
|
705
|
+
const loaded = await ValidatorModel.first({ id: model.id });
|
|
706
|
+
assert(loaded !== undefined && loaded.renamed === "value", "Renamed field should work");
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "@Serialize transforms to/from DB",
|
|
711
|
+
fn: async () => {
|
|
712
|
+
const data = { key: "value", nested: { a: 1 } };
|
|
713
|
+
const model = await ValidatorModel.create({ transformed: "test", absolute: 5, metadata: data, multi_validated: 50, renamed: "x" });
|
|
714
|
+
const loaded = await ValidatorModel.first({ id: model.id });
|
|
715
|
+
assert(loaded !== undefined && loaded.metadata.key === "value", "Serialized data should be restored");
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
// =========================================================================
|
|
719
|
+
// EDGE CASES
|
|
720
|
+
// =========================================================================
|
|
721
|
+
{
|
|
722
|
+
name: "@Validate accepts boundary value (exactly 3 chars)",
|
|
723
|
+
fn: async () => {
|
|
724
|
+
const user = await User.create({ name: "ABC", email: "abc@test.com" });
|
|
725
|
+
assert(user.name === "ABC", "Boundary value should be accepted");
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "first returns undefined for non-existent record",
|
|
730
|
+
fn: async () => {
|
|
731
|
+
const result = await User.first({ id: "non-existent-xyz" });
|
|
732
|
+
assert(result === undefined, "Should return undefined");
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "where returns empty array for no matches",
|
|
737
|
+
fn: async () => {
|
|
738
|
+
const results = await User.where({ id: "non-existent-xyz" });
|
|
739
|
+
assert(results.length === 0, "Should return empty array");
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: "partial update preserves other fields",
|
|
744
|
+
fn: async () => {
|
|
745
|
+
const user = users[3];
|
|
746
|
+
const original_email = user.email;
|
|
747
|
+
user.name = "Partially Updated";
|
|
748
|
+
await user.save();
|
|
749
|
+
const loaded = await User.first({ id: user.id });
|
|
750
|
+
assert(loaded !== undefined && loaded.name === "Partially Updated" && loaded.email === original_email, "Email should be preserved");
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: "include with no matches returns empty results",
|
|
755
|
+
fn: async () => {
|
|
756
|
+
const results = await User.where({ id: "non-existent-xyz" }, { include: { orders: true } });
|
|
757
|
+
assert(results.length === 0, "Should return empty array");
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
];
|
|
761
|
+
// =========================================================================
|
|
762
|
+
// RUN TESTS
|
|
763
|
+
// =========================================================================
|
|
764
|
+
for (const test of tests) {
|
|
765
|
+
try {
|
|
766
|
+
await test.fn();
|
|
767
|
+
console.log(` ✓ ${test.name}`);
|
|
768
|
+
passed++;
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
console.log(` ✗ ${test.name}`);
|
|
772
|
+
failures.push(`${test.name}: ${error.message}`);
|
|
773
|
+
failed++;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
console.error(`\nFatal error: ${error.message}`);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const duration = ((performance.now() - start) / 1000).toFixed(2);
|
|
782
|
+
console.log(`\n${passed + failed} tests | ${passed} passed | ${failed} failed | ${duration}s\n`);
|
|
783
|
+
if (failures.length > 0) {
|
|
784
|
+
console.log("Failures:");
|
|
785
|
+
failures.forEach((f) => console.log(` ${f}`));
|
|
786
|
+
}
|
|
787
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
788
|
+
})();
|
|
789
|
+
//# sourceMappingURL=index.test.js.map
|