stabilize-orm 1.3.9 → 2.1.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.
Files changed (78) hide show
  1. package/README.md +1097 -561
  2. package/dist/auto-migrate.d.ts +34 -0
  3. package/dist/auto-migrate.d.ts.map +1 -0
  4. package/dist/auto-migrate.js +3003 -0
  5. package/dist/auto-migrate.js.map +163 -0
  6. package/dist/cache.d.ts +90 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +166 -0
  9. package/dist/cache.js.map +64 -0
  10. package/dist/client.d.ts +73 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +2997 -0
  13. package/dist/client.js.map +162 -0
  14. package/dist/hooks.d.ts +31 -0
  15. package/dist/hooks.d.ts.map +1 -0
  16. package/dist/hooks.js +4 -0
  17. package/dist/hooks.js.map +11 -0
  18. package/dist/index.d.ts +101 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3183 -0
  21. package/dist/index.js.map +222 -0
  22. package/dist/logger.d.ts +40 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +8 -0
  25. package/dist/logger.js.map +11 -0
  26. package/dist/migrations.d.ts +31 -0
  27. package/dist/migrations.d.ts.map +1 -0
  28. package/dist/migrations.js +3009 -0
  29. package/dist/migrations.js.map +164 -0
  30. package/{model.ts → dist/model.d.ts} +124 -189
  31. package/dist/model.d.ts.map +1 -0
  32. package/dist/model.js +4 -0
  33. package/dist/model.js.map +10 -0
  34. package/dist/query-builder.d.ts +91 -0
  35. package/dist/query-builder.d.ts.map +1 -0
  36. package/dist/query-builder.js +14 -0
  37. package/dist/query-builder.js.map +12 -0
  38. package/dist/repository.d.ts +165 -0
  39. package/dist/repository.d.ts.map +1 -0
  40. package/dist/repository.js +176 -0
  41. package/dist/repository.js.map +69 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -0
  43. package/dist/types.d.ts +110 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +4 -0
  46. package/dist/types.js.map +10 -0
  47. package/dist/utils/encryption.d.ts +13 -0
  48. package/dist/utils/encryption.d.ts.map +1 -0
  49. package/dist/utils/encryption.js +4 -0
  50. package/dist/utils/encryption.js.map +10 -0
  51. package/package.json +96 -17
  52. package/.eslintrc.json +0 -10
  53. package/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -23
  54. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -25
  55. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  56. package/.github/workflows/ci-cd.yml +0 -22
  57. package/CHANGELOG.md +0 -75
  58. package/CODE_OF_CONDUCT.md +0 -87
  59. package/CONTRIBUTING.md +0 -48
  60. package/FUNDING.md +0 -14
  61. package/SECURITY.md +0 -35
  62. package/SUPPORT.md +0 -18
  63. package/bun.lock +0 -667
  64. package/cache.ts +0 -181
  65. package/client.ts +0 -249
  66. package/docker-compose.yml +0 -22
  67. package/hooks.ts +0 -76
  68. package/index.ts +0 -158
  69. package/logger.ts +0 -127
  70. package/migrations.ts +0 -318
  71. package/public/logo_both-transparent.png +0 -0
  72. package/public/logo_iamge-transparent.png +0 -0
  73. package/public/logo_text-transparent.png +0 -0
  74. package/query-builder.ts +0 -209
  75. package/repository.ts +0 -1096
  76. package/tests/migrations.test.ts +0 -141
  77. package/tsconfig.json +0 -32
  78. package/types.ts +0 -106
package/README.md CHANGED
@@ -1,561 +1,1097 @@
1
- # Stabilize ORM
2
-
3
- _A Modern, Type-Safe, and Expressive ORM for Bun_
4
- <p align="left">
5
- <img src="./public/logo_both-transparent.png" alt="Stabilize ORM Logo" width="280" />
6
- </p>
7
-
8
- <p align="left">
9
- <a href="https://www.npmjs.com/package/stabilize-orm"><img src="https://img.shields.io/npm/v/stabilize-orm.svg?label=version&color=blue" alt="NPM Version"></a>
10
- <a href="https://github.com/ElectronSz/stabilize-cli/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/stabilize-orm.svg?color=green" alt="License"></a>
11
- <a href="https://github.com/ElectronSz/stabilize-cli"><img src="https://img.shields.io/badge/Cli-Stabilize%201.2.0-blue.svg" alt="Stabilize CLI"></a>
12
- <a href="#"><img src="https://img.shields.io/badge/PostgreSQL-supported-blue" alt="PostgreSQL"></a>
13
- <a href="#"><img src="https://img.shields.io/badge/MySQL-supported-blue" alt="MySQL"></a>
14
- <a href="#"><img src="https://img.shields.io/badge/SQLite-supported-blue" alt="SQLite"></a>
15
- <a href="https://github.com/ElectronSz/stabilize-orm/actions/workflows/ci-cd.yml">
16
- <img src="https://github.com/ElectronSz/stabilize-orm/actions/workflows/ci-cd.yml/badge.svg" alt="Build Status">
17
- </a>
18
- <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="MIT License"></a>
19
- </p>
20
-
21
-
22
- **Stabilize** is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for **PostgreSQL**, **MySQL**, and **SQLite**. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.
23
-
24
- ---
25
-
26
- ## 🚀 Features
27
-
28
- - **Unified API**: Write once, run on PostgreSQL, MySQL, or SQLite.
29
- - **Programmatic Model Definitions**: Define models and columns using the `defineModel` API with the `DataTypes` enum for database-agnostic schemas.
30
- - **Full-Featured CLI**: Generate models, manage migrations, seed data, and reset your database from the command line with [stabilize-cli](https://github.com/ElectronSz/stabilize-cli).
31
- - **Automatic Migrations**: Generate database-specific SQL schemas directly from your model definitions.
32
- - **Versioned Models & Time-Travel**: Enable versioning in your model configuration for automatic history tables and snapshot queries.
33
- - **Retry Logic**: Automatic exponential backoff for database queries to handle transient connection issues.
34
- - **Connection Pooling**: Efficient connection management for PostgreSQL and MySQL.
35
- - **Transactional Integrity**: Built-in support for atomic transactions with automatic rollback on failure.
36
- - **Advanced Query Builder**: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
37
- - **Model Relationships**: Define `OneToOne`, `ManyToOne`, `OneToMany`, and `ManyToMany` relationships in the model configuration.
38
- - **Soft Deletes**: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
39
- - **Lifecycle Hooks**: Define hooks in the model configuration or as class methods for lifecycle events like `beforeCreate`, `afterUpdate`, etc.
40
- - **Pluggable Logging**: Includes a robust `StabilizeLogger` with support for file-based, rotating logs.
41
- - **Custom Errors**: `StabilizeError` provides clear, consistent error handling.
42
- - **Caching Layer**: Optional Redis-backed caching with `cache-aside` and `write-through` strategies.
43
- - **Custom Query Scopes**: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
44
- - **Timestamps**: Automatically manage `createdAt` and `updatedAt` columns for tracking record creation and update times.
45
-
46
- ---
47
-
48
- ## 📦 Installation
49
-
50
- Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).
51
-
52
- ```bash
53
- # Using Bun
54
- bun add stabilize-orm
55
-
56
- # Using npm
57
- npm install stabilize-orm
58
- ```
59
-
60
- ---
61
-
62
- ## 📃 Documentation & Community
63
-
64
- - [Changelog](./CHANGELOG.md)
65
- - [License](./LICENSE.md)
66
- - [Code of Conduct](./CODE_OF_CONDUCT.md)
67
- - [Contributing Guide](./CONTRIBUTING.md)
68
- - [Security Policy](./SECURITY.md)
69
- - [Support](./SUPPORT.md)
70
- - [Funding](./FUNDING.md)
71
-
72
- ---
73
-
74
- ## ⚙️ Configuration
75
-
76
- Create a database configuration file.
77
-
78
- ```typescript
79
- // config/database.ts
80
- import { DBType, type DBConfig } from "stabilize-orm";
81
-
82
- const dbConfig: DBConfig = {
83
- type: DBType.Postgres,
84
- connectionString: process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
85
- retryAttempts: 3,
86
- retryDelay: 1000,
87
- };
88
-
89
- export default dbConfig;
90
- ```
91
-
92
- Next, create a central ORM instance for your application.
93
-
94
- ```typescript
95
- // db.ts
96
- import { Stabilize, type CacheConfig, type LoggerConfig, LogLevel } from "stabilize-orm";
97
- import dbConfig from "./database";
98
-
99
- const cacheConfig: CacheConfig = {
100
- enabled: process.env.CACHE_ENABLED === "true",
101
- redisUrl: process.env.REDIS_URL,
102
- ttl: 60,
103
- };
104
-
105
- const loggerConfig: LoggerConfig = {
106
- level: LogLevel.Info,
107
- filePath: "logs/stabilize.log",
108
- maxFileSize: 5 * 1024 * 1024, // 5MB
109
- maxFiles: 3,
110
- };
111
-
112
- export const orm = new Stabilize(dbConfig, cacheConfig, loggerConfig);
113
- ```
114
-
115
- ---
116
-
117
- ## 🏗️ Models & Relationships
118
-
119
- Define your tables as classes using the `defineModel` function. The `DataTypes` enum ensures database-agnostic schemas.
120
-
121
- ### Example: Users and Roles (Many-to-Many) with Versioning
122
-
123
- ```typescript
124
- // models/User.ts
125
- import { defineModel, DataTypes, RelationType } from "stabilize-orm";
126
- import { UserRole } from "./UserRole";
127
-
128
- const User = defineModel({
129
- tableName: "users",
130
- versioned: true,
131
- columns: {
132
- id: { type: DataTypes.Integer, required: true },
133
- email: { type: DataTypes.String, length: 100, required: true, unique: true },
134
- },
135
- relations: [
136
- {
137
- type: RelationType.OneToMany,
138
- target: () => UserRole,
139
- property: "roles",
140
- foreignKey: "userId",
141
- },
142
- ],
143
- hooks: {
144
- beforeCreate: (entity) => console.log(`Creating user: ${entity.email}`),
145
- },
146
- });
147
-
148
- // Add a hook as a class method
149
- User.prototype.afterCreate = async function () {
150
- console.log(`Created user with ID: ${this.id}`);
151
- };
152
-
153
- export { User };
154
- ```
155
-
156
- ```typescript
157
- // models/Role.ts
158
- import { defineModel, DataTypes } from "stabilize-orm";
159
-
160
- const Role = defineModel({
161
- tableName: "roles",
162
- columns: {
163
- id: { type: DataTypes.Integer, required: true },
164
- name: { type: DataTypes.String, length: 50, required: true, unique: true },
165
- },
166
- });
167
-
168
- export { Role };
169
- ```
170
-
171
- ```typescript
172
- // models/UserRole.ts
173
- import { defineModel, DataTypes, RelationType } from "stabilize-orm";
174
- import { User } from "./User";
175
- import { Role } from "./Role";
176
-
177
- const UserRole = defineModel({
178
- tableName: "user_roles",
179
- columns: {
180
- id: { type: DataTypes.Integer, required: true },
181
- userId: { type: DataTypes.Integer, required: true, index: "idx_user_id" },
182
- roleId: { type: DataTypes.Integer, required: true, index: "idx_role_id" },
183
- },
184
- relations: [
185
- {
186
- type: RelationType.ManyToOne,
187
- target: () => User,
188
- property: "user",
189
- foreignKey: "userId",
190
- },
191
- {
192
- type: RelationType.ManyToOne,
193
- target: () => Role,
194
- property: "role",
195
- foreignKey: "roleId",
196
- },
197
- ],
198
- });
199
-
200
- export { UserRole };
201
- ```
202
-
203
- ---
204
-
205
- ## Versioning & Auditing
206
-
207
- Enable automatic history tracking and time-travel queries by setting `versioned: true` in your model configuration.
208
-
209
- - Each change is recorded in a `<table>_history` table with version, operation, and audit columns.
210
- - Supports snapshot queries, rollbacks, audits, and time-travel.
211
-
212
- ### **Versioning Example**
213
-
214
- ```typescript
215
- import { defineModel, DataTypes } from "stabilize-orm";
216
-
217
- const User = defineModel({
218
- tableName: "users",
219
- versioned: true,
220
- columns: {
221
- id: { type: DataTypes.Integer, required: true },
222
- name: { type: DataTypes.String, length: 100 },
223
- },
224
- });
225
-
226
- // --- Using versioning features:
227
-
228
- const userRepository = orm.getRepository(User);
229
-
230
- // Rollback to a previous version
231
- await userRepository.rollback(1, 3); // roll back user with id=1 to version 3
232
-
233
- // Get a snapshot as of a specific date
234
- const userAsOf = await userRepository.asOf(1, new Date("2025-01-01T00:00:00Z"));
235
- console.log(userAsOf);
236
-
237
- // View full version history
238
- const history = await userRepository.history(1);
239
- console.log(history);
240
- ```
241
-
242
- ---
243
-
244
- ## 🔄 Model Lifecycle Hooks
245
-
246
- Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.
247
-
248
- ### **Hooks Example**
249
-
250
- ```typescript
251
- import { defineModel, DataTypes } from "stabilize-orm";
252
-
253
- const User = defineModel({
254
- tableName: "users",
255
- columns: {
256
- id: { type: DataTypes.Integer, required: true },
257
- name: { type: DataTypes.String, length: 100 },
258
- createdAt: { type: DataTypes.DateTime },
259
- updatedAt: { type: DataTypes.DateTime },
260
- },
261
- hooks: {
262
- beforeCreate: (entity) => {
263
- entity.createdAt = new Date();
264
- },
265
- beforeUpdate: (entity) => {
266
- entity.updatedAt = new Date();
267
- },
268
- afterCreate: (entity) => {
269
- console.log(`User created: ${entity.name}`);
270
- },
271
- },
272
- });
273
-
274
- // Add a hook as a class method
275
- User.prototype.afterUpdate = async function () {
276
- console.log(`Updated user: ${this.name}`);
277
- };
278
-
279
- export { User };
280
- ```
281
-
282
- Supported hooks: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `beforeSave`, `afterSave`.
283
-
284
- ---
285
-
286
- ## 💻 Command-Line Interface (CLI)
287
-
288
- Stabilize includes a powerful CLI for managing your workflow. See: [stabilize-cli on GitHub](https://github.com/ElectronSz/stabilize-cli)
289
-
290
- ### Generating Files
291
-
292
- - **Generate a model**:
293
- ```bash
294
- stabilize-cli generate model Product
295
- ```
296
-
297
- - **Generate a migration from a model**:
298
- ```bash
299
- stabilize-cli generate migration User
300
- ```
301
-
302
- - **Generate a seed file**:
303
- ```bash
304
- stabilize-cli generate seed InitialRoles
305
- ```
306
-
307
- ### Database & Migration Management
308
-
309
- - **Run all pending migrations**:
310
- ```bash
311
- stabilize-cli migrate
312
- ```
313
-
314
- - **Roll back the last migration**:
315
- ```bash
316
- stabilize-cli migrate:rollback
317
- ```
318
-
319
- - **Run all pending seeds (in dependency order)**:
320
- ```bash
321
- stabilize-cli seed
322
- ```
323
-
324
- - **Check the status of migrations and seeds**:
325
- ```bash
326
- stabilize-cli status
327
- ```
328
-
329
- - **Reset the database (drop, migrate, seed)**:
330
- ```bash
331
- stabilize-cli db:reset
332
- ```
333
-
334
- ---
335
-
336
- ## 🧑‍💻 Querying Data
337
-
338
- ### Basic CRUD with Repositories
339
-
340
- ```typescript
341
- import { orm } from "./db";
342
- import { User } from "./models/User";
343
-
344
- const userRepository = orm.getRepository(User);
345
-
346
- const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
347
- const foundUser = await userRepository.findOne(newUser.id);
348
- const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
349
- await userRepository.delete(newUser.id);
350
- ```
351
-
352
- ### Advanced Queries with the Query Builder
353
-
354
- ```typescript
355
- const activeAdmins = await orm
356
- .getRepository(UserRole)
357
- .find()
358
- .join("users", "user_roles.user_id = users.id")
359
- .join("roles", "user_roles.role_id = roles.id")
360
- .select("users.id", "users.email", "roles.name as role_name")
361
- .where("roles.name = ?", "Admin")
362
- .orderBy("users.email ASC")
363
- .execute();
364
-
365
- console.log(activeAdmins);
366
- ```
367
-
368
- #### Query Builder API
369
-
370
- ```typescript
371
- {
372
- select(...fields: string[]): QueryBuilder<User>;
373
- where(condition: string, ...params: any[]): QueryBuilder<User>;
374
- join(table: string, condition: string): QueryBuilder<User>;
375
- orderBy(clause: string): QueryBuilder<User>;
376
- limit(limit: number): QueryBuilder<User>;
377
- offset(offset: number): QueryBuilder<User>;
378
- scope(name: string, ...args: any[]): QueryBuilder<User>;
379
- build(): { query: string; params: any[] };
380
- execute(client?: DBClient, cache?: Cache, cacheKey?: string): Promise<User[]>;
381
- }
382
- ```
383
-
384
- ### Custom Query Scopes
385
-
386
- Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the `scope` method on `Repository` or `QueryBuilder`, allowing you to chain them with other query operations.
387
-
388
- #### **Scopes Example**
389
-
390
- ```typescript
391
- import { defineModel, DataTypes } from "stabilize-orm";
392
- import { orm } from "./db";
393
-
394
- const User = defineModel({
395
- tableName: "users",
396
- columns: {
397
- id: { type: DataTypes.Integer, required: true },
398
- email: { type: DataTypes.String, length: 100, required: true },
399
- isActive: { type: DataTypes.Boolean, required: true },
400
- createdAt: { type: DataTypes.DateTime },
401
- updatedAt: { type: DataTypes.DateTime },
402
- },
403
- scopes: {
404
- active: (qb) => qb.where("isActive = ?", true),
405
- recent: (qb, days: number) => qb.where("createdAt >= ?", new Date(Date.now() - days * 24 * 60 * 60 * 1000)),
406
- },
407
- });
408
-
409
- const userRepository = orm.getRepository(User);
410
-
411
- // Fetch active users
412
- const activeUsers = await userRepository.scope("active").execute();
413
-
414
- // Fetch users created in the last 7 days
415
- const recentUsers = await userRepository.scope("recent", 7).execute();
416
-
417
- // Combine scopes with other query operations
418
- const recentActiveUsers = await userRepository
419
- .scope("active")
420
- .scope("recent", 7)
421
- .orderBy("createdAt DESC")
422
- .limit(10)
423
- .execute();
424
-
425
- console.log(recentActiveUsers);
426
- ```
427
-
428
- ### Timestamps
429
-
430
- Enable automatic management of `createdAt` and `updatedAt` columns by setting `timestamps` in your model configuration. The ORM automatically sets these fields during `create`, `update`, `bulkCreate`, `bulkUpdate`, and `upsert` operations in a TypeScript-safe manner, eliminating the need for manual hooks.
431
-
432
- #### **Timestamps Example**
433
-
434
- ```typescript
435
- import { defineModel, DataTypes } from "stabilize-orm";
436
- import { orm } from "./db";
437
-
438
- const User = defineModel({
439
- tableName: "users",
440
- columns: {
441
- id: { type: DataTypes.Integer, required: true },
442
- email: { type: DataTypes.String, length: 100, required: true },
443
- createdAt: { type: DataTypes.DateTime },
444
- updatedAt: { type: DataTypes.DateTime },
445
- },
446
- timestamps: {
447
- createdAt: "createdAt",
448
- updatedAt: "updatedAt",
449
- },
450
- });
451
-
452
- const userRepository = orm.getRepository(User);
453
-
454
- // Create a user (createdAt and updatedAt set automatically)
455
- const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
456
- console.log(newUser.createdAt, newUser.updatedAt); // Outputs current timestamp
457
-
458
- // Update a user (updatedAt updated automatically)
459
- const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
460
- console.log(updatedUser.updatedAt); // Outputs new timestamp
461
-
462
- // Bulk create users
463
- const newUsers = await userRepository.bulkCreate([
464
- { email: "user1@example.com" },
465
- { email: "user2@example.com" },
466
- ]);
467
- console.log(newUsers.map(u => u.createdAt)); // Outputs timestamps for each user
468
- ```
469
-
470
- ---
471
-
472
- ## 🗑️ Soft Deletes
473
-
474
- Enable soft deletes by setting `softDelete: true` and marking a column (e.g., `deletedAt`) with `softDelete: true` in the model configuration.
475
-
476
- - Use `repository.delete(id)` to mark an entity as deleted.
477
- - Use `repository.recover(id)` to restore a soft-deleted entity.
478
- - Queries automatically exclude soft-deleted rows unless specified otherwise.
479
-
480
- ### **Soft Delete Example**
481
-
482
- ```typescript
483
- import { defineModel, DataTypes } from "stabilize-orm";
484
-
485
- const User = defineModel({
486
- tableName: "users",
487
- softDelete: true,
488
- columns: {
489
- id: { type: DataTypes.Integer, required: true },
490
- email: { type: DataTypes.String, length: 100, required: true },
491
- deletedAt: { type: DataTypes.DateTime, softDelete: true },
492
- },
493
- });
494
-
495
- const userRepository = orm.getRepository(User);
496
- await userRepository.create({ email: "lwazicd@icloud.com" });
497
- await userRepository.delete(1); // Soft delete
498
- await userRepository.recover(1); // Recover
499
- ```
500
-
501
- ---
502
-
503
- ## 🌐 Express.js Integration
504
-
505
- Stabilize ORM works seamlessly with web frameworks like Express.
506
-
507
- ```typescript
508
- import express from "express";
509
- import { orm } from "./db";
510
- import { User } from "./models/User";
511
-
512
- const app = express();
513
- app.use(express.json());
514
-
515
- const userRepository = orm.getRepository(User);
516
-
517
- app.get("/users", async (req, res) => {
518
- try {
519
- const users = await userRepository.find().execute();
520
- res.json(users);
521
- } catch (err) {
522
- res.status(500).json({ error: "Failed to fetch users." });
523
- }
524
- });
525
-
526
- app.post("/users", async (req, res) => {
527
- try {
528
- const user = await userRepository.create(req.body);
529
- res.status(201).json(user);
530
- } catch (err) {
531
- res.status(500).json({ error: "User creation failed." });
532
- }
533
- });
534
-
535
- app.listen(3000, () => {
536
- console.log("Server listening on port 3000");
537
- });
538
- ```
539
-
540
- ---
541
-
542
- ## 🧑‍🔬 Testing & Time-Travel
543
-
544
- - Use time-travel queries to inspect historical entity states.
545
- - Assert audit trails and rollback operations in your tests.
546
-
547
- ---
548
-
549
- ## 📑 License
550
-
551
- Licensed under the MIT License. See [LICENSE.md](./LICENSE.md) for details.
552
-
553
- ---
554
-
555
- <div align="center">
556
-
557
- Created with ❤️ by **ElectronSz**
558
- <br/>
559
- <em>File last updated: 2025-10-19 11:12:00 SAST</em>
560
-
561
- </div>
1
+ # Stabilize ORM
2
+
3
+ _A Modern, Type-Safe, and Expressive ORM for Bun_
4
+
5
+ <p align="left">
6
+ <img src="./public/logo_both-transparent.png" alt="Stabilize ORM Logo" width="280" />
7
+ </p>
8
+
9
+ <p align="left">
10
+ <a href="https://www.npmjs.com/package/stabilize-orm"><img src="https://img.shields.io/npm/v/stabilize-orm.svg?label=version&color=blue" alt="NPM Version"></a>
11
+ <a href="https://github.com/ElectronSz/stabilize-cli/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/stabilize-orm.svg?color=green" alt="License"></a>
12
+ <a href="https://github.com/ElectronSz/stabilize-cli"><img src="https://img.shields.io/badge/Cli-Stabilize%202.1.0-blue.svg" alt="Stabilize CLI"></a>
13
+ <a href="#"><img src="https://img.shields.io/badge/PostgreSQL-supported-blue" alt="PostgreSQL"></a>
14
+ <a href="#"><img src="https://img.shields.io/badge/MySQL-supported-blue" alt="MySQL"></a>
15
+ <a href="#"><img src="https://img.shields.io/badge/SQLite-supported-blue" alt="SQLite"></a>
16
+ <a href="https://github.com/ElectronSz/stabilize-orm/actions/workflows/ci-cd.yml">
17
+ <img src="https://github.com/ElectronSz/stabilize-orm/actions/workflows/ci-cd.yml/badge.svg" alt="Build Status">
18
+ </a>
19
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="MIT License"></a>
20
+ </p>
21
+
22
+ **Stabilize** is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for **PostgreSQL**, **MySQL**, and **SQLite**. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.
23
+
24
+ ---
25
+
26
+ ## 🚀 Features
27
+
28
+ - **Unified API**: Write once, run on PostgreSQL, MySQL, or SQLite.
29
+ - **Programmatic Model Definitions**: Define models and columns using the `defineModel` API with the `DataTypes` enum for database-agnostic schemas.
30
+ - **Full-Featured CLI**: Generate models, manage migrations, seed data, and reset your database from the command line with [stabilize-cli](https://github.com/ElectronSz/stabilize-cli).
31
+ - **Automatic Migrations**: Generate database-specific SQL schemas directly from your model definitions.
32
+ - **Versioned Models & Time-Travel**: Enable versioning in your model configuration for automatic history tables and snapshot queries.
33
+ - **Retry Logic**: Automatic exponential backoff for database queries to handle transient connection issues.
34
+ - **Connection Pooling**: Efficient connection management for PostgreSQL and MySQL.
35
+ - **Transactional Integrity**: Built-in support for atomic transactions with automatic rollback on failure.
36
+ - **Advanced Query Builder**: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
37
+ - **Pagination Helper**: Easily paginate any query with `.paginate(page, pageSize)` and get `{ data, total, page, pageSize }`.
38
+ - **Advanced Model Validation**: Enforce rules like `required`, `minLength`, `maxLength`, `pattern`, and custom validators—errors are thrown on invalid input.
39
+ - **Model Relationships**: Define `OneToOne`, `ManyToOne`, `OneToMany`, and `ManyToMany` relationships in the model configuration.
40
+ - **Soft Deletes**: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
41
+ - **Lifecycle Hooks**: Define hooks in the model configuration or as class methods for lifecycle events like `beforeCreate`, `afterUpdate`, etc.
42
+ - **Pluggable Logging**: Includes a robust `StabilizeLogger` with support for file-based, rotating logs.
43
+ - **Custom Errors**: `StabilizeError` provides clear, consistent error handling.
44
+ - **Caching Layer**: Optional Redis-backed caching with `cache-aside` and `write-through` strategies.
45
+ - **Custom Query Scopes**: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
46
+ - **Timestamps**: Automatically manage `createdAt` and `updatedAt` columns for tracking record creation and update times.
47
+ - **SQL Default Expressions**: Support database-side default expressions (e.g., `gen_random_uuid()`, `NOW()`) for columns using the `sqlDefault()` helper.
48
+ - **Nested Relations (Eager Loading)**: Load deeply nested relations using dot notation like `"roles.permissions"`.
49
+ - **AutoMigrate with Index Management**: Automatically create, detect, and remove indexes and unique constraints during migration.
50
+ - **Advanced Query Builder Filters**: Chainable `.orWhere()`, `.whereIn()`, `.whereNotIn()`, `.whereNull()`, `.whereNotNull()`, `.whereBetween()`, `.groupBy()`, `.having()`, `.lock()` methods.
51
+ - **Optimistic Locking**: Add `optimisticLock: true` to a version column to automatically detect concurrent modification conflicts and throw `CONCURRENT_MODIFICATION` errors.
52
+ - **findAndCount**: Get paginated results with a total count in one call.
53
+ - **findOneBy / findBy**: TypeORM-style conditional finders without writing raw SQL.
54
+ - **Aggregate Queries**: Run `count()`, `sum()`, `avg()`, `min()`, `max()` directly from the repository or query builder.
55
+ - **Cursor-Based Pagination**: Efficient forward/backward cursor pagination for large datasets (Prisma-style `findMany`).
56
+ - **exists**: Check if a record exists without loading it.
57
+ - **recoverAll**: Bulk restore all soft-deleted records.
58
+ - **truncate**: Clear all rows from a table.
59
+ - **seed / defineSeed**: Laravel-style seeding framework with `defineSeed` and `runSeeds`.
60
+ - **resetDatabase**: Drop tables, re-migrate, and optionally re-seed for development.
61
+ - **healthCheck**: Get database and table health status with latency for monitoring endpoints.
62
+ - **rawQuery / rawExec**: Execute raw SQL directly from the `Stabilize` instance.
63
+ - **bulkUpsert**: Upsert multiple records in a single transaction.
64
+ - **findMany**: Prisma-style query with `where`, `cursor`, `take`, `skip`, `orderBy`.
65
+ - **countDistinct**: Count unique values in a column.
66
+ - **increment / decrement**: Atomically increment or decrement a numeric field.
67
+ - **pluck**: Get an array of a single column's values (Rails-style).
68
+ - **selectColumns**: Get only specific columns from a query.
69
+ - **toggle**: Toggle a boolean field (Rails-style).
70
+ - **updateBy / deleteBy**: Conditional bulk updates and deletes without writing SQL.
71
+ - **restoreBy**: Restore soft-deleted records matching conditions.
72
+ - **findDeleted / withTrashed**: Query soft-deleted or all records.
73
+ - **upsertMany**: Batch upsert in configurable batch sizes.
74
+ - **map / each / eachBatch**: Transform, iterate, and batch-process query results.
75
+ - **lockForUpdate**: Pessimistic row locking for read-modify-write.
76
+ - **firstOrCreate / updateOrCreate**: Laravel-style find-or-create patterns.
77
+ - **first / last / random**: Single-record shortcuts.
78
+ - **StabilizeEmitter**: Event system for `query`, `error`, `connection:open/close`, `transaction:start/complete/error`.
79
+ - **TransactionIsolationLevel**: Type for `READ UNCOMMITTED`, `READ COMMITTED`, `REPEATABLE READ`, `SERIALIZABLE`.
80
+ - **generateUUID**: Cross-runtime UUID generation helper.
81
+ - **poolStats**: Get connection pool statistics.
82
+ - **Database Backup & Restore**: `db:backup` and `db:restore` commands for database backup management.
83
+ - **API Generation**: `generate:api` command scaffolds full CRUD REST API routes from models.
84
+ - **Fresh Migrations**: `migrate:fresh` drops all tables and re-runs migrations without seeding.
85
+ - **Database Size Analysis**: `db:size` command shows table sizes and row count statistics.
86
+
87
+ ---
88
+
89
+ ## 📦 Installation
90
+
91
+ Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).
92
+
93
+ ```bash
94
+ # Using Bun
95
+ bun add stabilize-orm
96
+
97
+ # Using npm
98
+ npm install stabilize-orm
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 📃 Documentation & Community
104
+
105
+ - [Changelog](./CHANGELOG.md)
106
+ - [License](./LICENSE)
107
+ - [Code of Conduct](./CODE_OF_CONDUCT.md)
108
+ - [Contributing Guide](./CONTRIBUTING.md)
109
+ - [Security Policy](./SECURITY.md)
110
+ - [Support](./SUPPORT.md)
111
+ - [Funding](./FUNDING.md)
112
+
113
+ ### Examples
114
+
115
+ - [Blog](./examples/blog.ts) - Blog with users, posts, comments, and versioning
116
+ - [E-Commerce](./examples/ecommerce.ts) - Products, categories, orders with transactions
117
+ - [REST API](./examples/rest-api.ts) - Express.js REST API with pagination and optimistic locking
118
+ - [SaaS](./examples/saas.ts) - Multi-tenant SaaS with tenants, members, and scoped projects
119
+ - [CMS](./examples/cms.ts) - Content management with authors, categories, articles, and versioning
120
+ - [Analytics](./examples/analytics.ts) - Event tracking with aggregations and metrics
121
+
122
+ ---
123
+
124
+ ## ⚙️ Configuration
125
+
126
+ Create a database configuration file.
127
+
128
+ ```typescript
129
+ // config/database.ts
130
+ import { DBType, type DBConfig } from "stabilize-orm";
131
+
132
+ const dbConfig: DBConfig = {
133
+ type: DBType.Postgres,
134
+ connectionString:
135
+ process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
136
+ retryAttempts: 3,
137
+ retryDelay: 1000,
138
+ };
139
+
140
+ export default dbConfig;
141
+ ```
142
+
143
+ Next, create a central ORM instance for your application.
144
+
145
+ ```typescript
146
+ // db.ts
147
+ import {
148
+ Stabilize,
149
+ type CacheConfig,
150
+ type LoggerConfig,
151
+ LogLevel,
152
+ } from "stabilize-orm";
153
+ import dbConfig from "./database";
154
+
155
+ const cacheConfig: CacheConfig = {
156
+ enabled: process.env.CACHE_ENABLED === "true",
157
+ redisUrl: process.env.REDIS_URL,
158
+ ttl: 60,
159
+ };
160
+
161
+ const loggerConfig: LoggerConfig = {
162
+ level: LogLevel.Info,
163
+ filePath: "logs/stabilize.log",
164
+ maxFileSize: 5 * 1024 * 1024, // 5MB
165
+ maxFiles: 3,
166
+ };
167
+
168
+ export const orm = new Stabilize(dbConfig, cacheConfig, loggerConfig);
169
+ ```
170
+
171
+ ---
172
+
173
+ ## 🏗️ Models & Relationships
174
+
175
+ Define your tables as classes using the `defineModel` function. The `DataTypes` enum ensures database-agnostic schemas.
176
+
177
+ ### Example: Users and Roles (Many-to-Many) with Versioning
178
+
179
+ ```typescript
180
+ // models/User.ts
181
+ import { defineModel, DataTypes, RelationType } from "stabilize-orm";
182
+ import { UserRole } from "./UserRole";
183
+
184
+ const User = defineModel({
185
+ tableName: "users",
186
+ versioned: true,
187
+ columns: {
188
+ id: { type: DataTypes.Integer, required: true },
189
+ email: {
190
+ type: DataTypes.String,
191
+ length: 100,
192
+ required: true,
193
+ unique: true,
194
+ },
195
+ },
196
+ relations: [
197
+ {
198
+ type: RelationType.OneToMany,
199
+ target: () => UserRole,
200
+ property: "roles",
201
+ foreignKey: "userId",
202
+ },
203
+ ],
204
+ hooks: {
205
+ beforeCreate: (entity) => console.log(`Creating user: ${entity.email}`),
206
+ },
207
+ });
208
+
209
+ export { User };
210
+ ```
211
+
212
+ ---
213
+
214
+ ## 🔍 Pagination
215
+
216
+ The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.
217
+
218
+ ```typescript
219
+ const users = await userRepository.find().paginate(2, 10).execute(dbClient);
220
+ // users = [...] // Returns an array of rows for that page
221
+ ```
222
+
223
+ Or, use the query builder:
224
+
225
+ ```typescript
226
+ const page = await userRepository
227
+ .find()
228
+ .where("isActive = ?", true)
229
+ .paginate(1, 20)
230
+ .execute();
231
+ ```
232
+
233
+ ---
234
+
235
+ ## 🛡️ Advanced Validation
236
+
237
+ Models can define advanced validation rules for columns, including:
238
+
239
+ - `required`
240
+ - `minLength` / `maxLength`
241
+ - `pattern` (RegExp)
242
+ - `customValidator` (function)
243
+
244
+ Validation errors are thrown on create/update if data is invalid.
245
+
246
+ ```typescript
247
+ const User = defineModel({
248
+ tableName: "users",
249
+ columns: {
250
+ id: { type: DataTypes.Integer, required: true },
251
+ email: {
252
+ type: DataTypes.String,
253
+ required: true,
254
+ unique: true,
255
+ minLength: 6,
256
+ pattern: /^[^@]+@[^@]+\.[^@]+$/,
257
+ customValidator: (val) =>
258
+ val.endsWith("@offbytesecure.com") ||
259
+ "Must use an @offbytesecure.com email",
260
+ },
261
+ password: { type: DataTypes.String, minLength: 8 },
262
+ },
263
+ });
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Versioning & Auditing
269
+
270
+ Enable automatic history tracking and time-travel queries by setting `versioned: true` in your model configuration.
271
+
272
+ - Each change is recorded in a `<table>_history` table with version, operation, and audit columns.
273
+ - Supports snapshot queries, rollbacks, audits, and time-travel.
274
+
275
+ ### **Versioning Example**
276
+
277
+ ```typescript
278
+ import { defineModel, DataTypes } from "stabilize-orm";
279
+
280
+ const User = defineModel({
281
+ tableName: "users",
282
+ versioned: true,
283
+ columns: {
284
+ id: { type: DataTypes.Integer, required: true },
285
+ name: { type: DataTypes.String, length: 100 },
286
+ },
287
+ });
288
+
289
+ // --- Using versioning features:
290
+
291
+ const userRepository = orm.getRepository(User);
292
+
293
+ // Rollback to a previous version
294
+ await userRepository.rollback(1, 3); // roll back user with id=1 to version 3
295
+
296
+ // Get a snapshot as of a specific date
297
+ const userAsOf = await userRepository.asOf(1, new Date("2025-01-01T00:00:00Z"));
298
+ console.log(userAsOf);
299
+
300
+ // View full version history
301
+ const history = await userRepository.history(1);
302
+ console.log(history);
303
+ ```
304
+
305
+ ---
306
+
307
+ ## 🔄 Model Lifecycle Hooks
308
+
309
+ Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.
310
+
311
+ ### **Hooks Example**
312
+
313
+ ```typescript
314
+ import { defineModel, DataTypes } from "stabilize-orm";
315
+
316
+ const User = defineModel({
317
+ tableName: "users",
318
+ columns: {
319
+ id: { type: DataTypes.Integer, required: true },
320
+ name: { type: DataTypes.String, length: 100 },
321
+ createdAt: { type: DataTypes.DateTime },
322
+ updatedAt: { type: DataTypes.DateTime },
323
+ },
324
+ hooks: {
325
+ beforeCreate: (entity) => {
326
+ entity.createdAt = new Date();
327
+ },
328
+ beforeUpdate: (entity) => {
329
+ entity.updatedAt = new Date();
330
+ },
331
+ afterCreate: (entity) => {
332
+ console.log(`User created: ${entity.name}`);
333
+ },
334
+ },
335
+ });
336
+
337
+ // Add a hook as a class method
338
+ User.prototype.afterUpdate = async function () {
339
+ console.log(`Updated user: ${this.name}`);
340
+ };
341
+
342
+ export { User };
343
+ ```
344
+
345
+ Supported hooks: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `beforeSave`, `afterSave`.
346
+
347
+ ---
348
+
349
+ ## 💻 Command-Line Interface (CLI)
350
+
351
+ Stabilize includes a powerful CLI for managing your workflow. See: [stabilize-cli on GitHub](https://github.com/ElectronSz/stabilize-cli)
352
+
353
+ ### Generating Files
354
+
355
+ - **Generate a model**:
356
+
357
+ ```bash
358
+ stabilize-cli generate:model Product
359
+ ```
360
+
361
+ - **Generate a migration from a model**:
362
+
363
+ ```bash
364
+ stabilize-cli generate:migration User
365
+ ```
366
+
367
+ - **Generate a seed file**:
368
+
369
+ ```bash
370
+ stabilize-cli generate:seed InitialRoles
371
+ ```
372
+
373
+ - **Generate a REST API scaffold**:
374
+ ```bash
375
+ stabilize-cli generate:api User
376
+ ```
377
+
378
+ ### Database & Migration Management
379
+
380
+ - **Run all pending migrations**:
381
+
382
+ ```bash
383
+ stabilize-cli migrate
384
+ ```
385
+
386
+ - **Roll back the last migration**:
387
+
388
+ ```bash
389
+ stabilize-cli migrate:rollback
390
+ ```
391
+
392
+ - **Fresh migration (drop + re-migrate)**:
393
+
394
+ ```bash
395
+ stabilize-cli migrate:fresh --force
396
+ ```
397
+
398
+ - **Run all pending seeds (in dependency order)**:
399
+
400
+ ```bash
401
+ stabilize-cli seed
402
+ ```
403
+
404
+ - **Check the status of migrations and seeds**:
405
+
406
+ ```bash
407
+ stabilize-cli status
408
+ ```
409
+
410
+ - **Reset the database (drop, migrate, seed)**:
411
+ ```bash
412
+ stabilize-cli db:reset
413
+ ```
414
+
415
+ ### Backup & Restore
416
+
417
+ - **Backup the database**:
418
+
419
+ ```bash
420
+ stabilize-cli db:backup
421
+ ```
422
+
423
+ - **Restore from a backup**:
424
+ ```bash
425
+ stabilize-cli db:restore backups/backup_20250101120000.db --force
426
+ ```
427
+
428
+ ### Diagnostics
429
+
430
+ - **Database size statistics**:
431
+
432
+ ```bash
433
+ stabilize-cli db:size
434
+ ```
435
+
436
+ - **Health check**:
437
+
438
+ ```bash
439
+ stabilize-cli health
440
+ ```
441
+
442
+ - **CLI info**:
443
+ ```bash
444
+ stabilize-cli info
445
+ ```
446
+
447
+ ---
448
+
449
+ ## 🧑‍💻 Querying Data
450
+
451
+ ### Basic CRUD with Repositories
452
+
453
+ ```typescript
454
+ import { orm } from "./db";
455
+ import { User } from "./models/User";
456
+
457
+ const userRepository = orm.getRepository(User);
458
+
459
+ const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
460
+ const foundUser = await userRepository.findOne(newUser.id);
461
+ const updatedUser = await userRepository.update(newUser.id, {
462
+ email: "admin@offbytesecure.com",
463
+ });
464
+ await userRepository.delete(newUser.id);
465
+ ```
466
+
467
+ ### Advanced Queries with the Query Builder
468
+
469
+ ```typescript
470
+ const activeAdmins = await orm
471
+ .getRepository(UserRole)
472
+ .find()
473
+ .join("users", "user_roles.user_id = users.id")
474
+ .join("roles", "user_roles.role_id = roles.id")
475
+ .select("users.id", "users.email", "roles.name as role_name")
476
+ .where("roles.name = ?", "Admin")
477
+ .orderBy("users.email ASC")
478
+ .execute();
479
+
480
+ console.log(activeAdmins);
481
+ ```
482
+
483
+ #### Query Builder API
484
+
485
+ ```typescript
486
+ {
487
+ select(...fields: string[]): QueryBuilder<User>;
488
+ where(condition: string, ...params: any[]): QueryBuilder<User>;
489
+ orWhere(condition: string, ...params: any[]): QueryBuilder<User>;
490
+ whereIn(column: string, values: any[]): QueryBuilder<User>;
491
+ whereNotIn(column: string, values: any[]): QueryBuilder<User>;
492
+ whereNull(column: string): QueryBuilder<User>;
493
+ whereNotNull(column: string): QueryBuilder<User>;
494
+ whereBetween(column: string, start: any, end: any): QueryBuilder<User>;
495
+ groupBy(clause: string): QueryBuilder<User>;
496
+ having(condition: string, ...params: any[]): QueryBuilder<User>;
497
+ join(table: string, condition: string): QueryBuilder<User>;
498
+ orderBy(clause: string): QueryBuilder<User>;
499
+ limit(limit: number): QueryBuilder<User>;
500
+ offset(offset: number): QueryBuilder<User>;
501
+ lock(mode?: "FOR UPDATE" | "FOR SHARE"): QueryBuilder<User>;
502
+ withRelations(...relations: string[]): QueryBuilder<User>;
503
+ scope(name: string, ...args: any[]): QueryBuilder<User>;
504
+ paginate(page: number, pageSize: number): QueryBuilder<User>;
505
+ build(): { query: string; params: any[] };
506
+ execute(client?: DBClient, cache?: Cache, cacheKey?: string): Promise<User[]>;
507
+ }
508
+ ```
509
+
510
+ ### Custom Query Scopes
511
+
512
+ Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the `scope` method on `Repository` or `QueryBuilder`, allowing you to chain them with other query operations.
513
+
514
+ #### **Scopes Example**
515
+
516
+ ```typescript
517
+ import { defineModel, DataTypes } from "stabilize-orm";
518
+ import { orm } from "./db";
519
+
520
+ const User = defineModel({
521
+ tableName: "users",
522
+ columns: {
523
+ id: { type: DataTypes.Integer, required: true },
524
+ email: { type: DataTypes.String, length: 100, required: true },
525
+ isActive: { type: DataTypes.Boolean, required: true },
526
+ createdAt: { type: DataTypes.DateTime },
527
+ updatedAt: { type: DataTypes.DateTime },
528
+ },
529
+ scopes: {
530
+ active: (qb) => qb.where("isActive = ?", true),
531
+ recent: (qb, days: number) =>
532
+ qb.where(
533
+ "createdAt >= ?",
534
+ new Date(Date.now() - days * 24 * 60 * 60 * 1000),
535
+ ),
536
+ },
537
+ });
538
+
539
+ const userRepository = orm.getRepository(User);
540
+
541
+ // Fetch active users
542
+ const activeUsers = await userRepository.scope("active").execute();
543
+
544
+ // Fetch users created in the last 7 days
545
+ const recentUsers = await userRepository.scope("recent", 7).execute();
546
+
547
+ // Combine scopes with other query operations
548
+ const recentActiveUsers = await userRepository
549
+ .scope("active")
550
+ .scope("recent", 7)
551
+ .orderBy("createdAt DESC")
552
+ .limit(10)
553
+ .execute();
554
+
555
+ console.log(recentActiveUsers);
556
+ ```
557
+
558
+ ### Timestamps
559
+
560
+ Enable automatic management of `createdAt` and `updatedAt` columns by setting `timestamps` in your model configuration. The ORM automatically sets these fields during `create`, `update`, `bulkCreate`, `bulkUpdate`, and `upsert` operations in a TypeScript-safe manner, eliminating the need for manual hooks.
561
+
562
+ #### **Timestamps Example**
563
+
564
+ ```typescript
565
+ import { defineModel, DataTypes } from "stabilize-orm";
566
+ import { orm } from "./db";
567
+
568
+ const User = defineModel({
569
+ tableName: "users",
570
+ columns: {
571
+ id: { type: DataTypes.Integer, required: true },
572
+ email: { type: DataTypes.String, length: 100, required: true },
573
+ createdAt: { type: DataTypes.DateTime },
574
+ updatedAt: { type: DataTypes.DateTime },
575
+ },
576
+ timestamps: {
577
+ createdAt: "createdAt",
578
+ updatedAt: "updatedAt",
579
+ },
580
+ });
581
+
582
+ const userRepository = orm.getRepository(User);
583
+
584
+ // Create a user (createdAt and updatedAt set automatically)
585
+ const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
586
+ console.log(newUser.createdAt, newUser.updatedAt); // Outputs current timestamp
587
+
588
+ // Update a user (updatedAt updated automatically)
589
+ const updatedUser = await userRepository.update(newUser.id, {
590
+ email: "admin@offbytesecure.com",
591
+ });
592
+ console.log(updatedUser.updatedAt); // Outputs new timestamp
593
+
594
+ // Bulk create users
595
+ const newUsers = await userRepository.bulkCreate([
596
+ { email: "user1@example.com" },
597
+ { email: "user2@example.com" },
598
+ ]);
599
+ console.log(newUsers.map((u) => u.createdAt)); // Outputs timestamps for each user
600
+ ```
601
+
602
+ ---
603
+
604
+ ## 🗑️ Soft Deletes
605
+
606
+ Enable soft deletes by setting `softDelete: true` and marking a column (e.g., `deletedAt`) with `softDelete: true` in the model configuration.
607
+
608
+ - Use `repository.delete(id)` to mark an entity as deleted.
609
+ - Use `repository.recover(id)` to restore a soft-deleted entity.
610
+ - Queries automatically exclude soft-deleted rows unless specified otherwise.
611
+
612
+ ### **Soft Delete Example**
613
+
614
+ ```typescript
615
+ import { defineModel, DataTypes } from "stabilize-orm";
616
+
617
+ const User = defineModel({
618
+ tableName: "users",
619
+ softDelete: true,
620
+ columns: {
621
+ id: { type: DataTypes.Integer, required: true },
622
+ email: { type: DataTypes.String, length: 100, required: true },
623
+ deletedAt: { type: DataTypes.DateTime, softDelete: true },
624
+ },
625
+ });
626
+
627
+ const userRepository = orm.getRepository(User);
628
+ await userRepository.create({ email: "lwazicd@icloud.com" });
629
+ await userRepository.delete(1); // Soft delete
630
+ await userRepository.recover(1); // Recover
631
+ ```
632
+
633
+ ---
634
+
635
+ ## 🌐 Express.js Integration
636
+
637
+ Stabilize ORM works seamlessly with web frameworks like Express.
638
+
639
+ ```typescript
640
+ import express from "express";
641
+ import { orm } from "./db";
642
+ import { User } from "./models/User";
643
+
644
+ const app = express();
645
+ app.use(express.json());
646
+
647
+ const userRepository = orm.getRepository(User);
648
+
649
+ app.get("/users", async (req, res) => {
650
+ try {
651
+ const users = await userRepository.find().execute();
652
+ res.json(users);
653
+ } catch (err) {
654
+ res.status(500).json({ error: "Failed to fetch users." });
655
+ }
656
+ });
657
+
658
+ app.post("/users", async (req, res) => {
659
+ try {
660
+ const user = await userRepository.create(req.body);
661
+ res.status(201).json(user);
662
+ } catch (err) {
663
+ res.status(500).json({ error: "User creation failed." });
664
+ }
665
+ });
666
+
667
+ app.listen(3000, () => {
668
+ console.log("Server listening on port 3000");
669
+ });
670
+ ```
671
+
672
+ ---
673
+
674
+ ## 🧑‍🔬 Testing & Time-Travel
675
+
676
+ - Use time-travel queries to inspect historical entity states.
677
+ - Assert audit trails and rollback operations in your tests.
678
+
679
+ ---
680
+
681
+ ## 🔒 Optimistic Locking
682
+
683
+ Enable optimistic locking to detect concurrent modifications. Add `optimisticLock: true` to a version column in your model.
684
+
685
+ ```typescript
686
+ import { defineModel, DataTypes } from "stabilize-orm";
687
+
688
+ const User = defineModel({
689
+ tableName: "users",
690
+ columns: {
691
+ id: { type: DataTypes.Integer, required: true },
692
+ name: { type: DataTypes.String },
693
+ version: { type: DataTypes.Integer, optimisticLock: true },
694
+ },
695
+ });
696
+
697
+ const userRepository = orm.getRepository(User);
698
+
699
+ // Create with initial version
700
+ const user = await userRepository.create({ name: "Lwazi", version: 1 });
701
+
702
+ // Update - version is automatically incremented
703
+ // If another transaction modified the record, a CONCURRENT_MODIFICATION error is thrown
704
+ try {
705
+ await userRepository.update(user.id, {
706
+ name: "Updated",
707
+ version: user.version,
708
+ });
709
+ } catch (err) {
710
+ if (err.code === "CONCURRENT_MODIFICATION") {
711
+ console.log("Record was modified by another transaction");
712
+ }
713
+ }
714
+ ```
715
+
716
+ ---
717
+
718
+ ## ⏱️ SQL Default Expressions
719
+
720
+ Use `sqlDefault()` to set database-side default values for columns (e.g., `gen_random_uuid()`, `NOW()`).
721
+
722
+ ```typescript
723
+ import { defineModel, DataTypes, sqlDefault } from "stabilize-orm";
724
+
725
+ const User = defineModel({
726
+ tableName: "users",
727
+ columns: {
728
+ id: {
729
+ type: DataTypes.UUID,
730
+ required: true,
731
+ defaultExpression: sqlDefault("gen_random_uuid()"),
732
+ },
733
+ name: { type: DataTypes.String },
734
+ createdAt: {
735
+ type: DataTypes.DateTime,
736
+ defaultExpression: sqlDefault("NOW()"),
737
+ },
738
+ },
739
+ });
740
+ ```
741
+
742
+ ---
743
+
744
+ ## 📊 Advanced Query Builder
745
+
746
+ The query builder now supports additional filter methods:
747
+
748
+ ```typescript
749
+ const results = await userRepository
750
+ .find()
751
+ .where("status = ?", "active")
752
+ .orWhere("role = ?", "admin")
753
+ .whereIn("age", [25, 30, 35])
754
+ .whereBetween("createdAt", new Date("2025-01-01"), new Date("2025-12-31"))
755
+ .whereNull("deletedAt")
756
+ .groupBy("department")
757
+ .having("COUNT(*) > ?", 5)
758
+ .orderBy("createdAt DESC")
759
+ .limit(10)
760
+ .execute();
761
+ ```
762
+
763
+ ---
764
+
765
+ ## 🔗 Nested Relations
766
+
767
+ Load deeply nested relations using dot notation:
768
+
769
+ ```typescript
770
+ const user = await userRepository.findOne(1, {
771
+ relations: ["roles", "roles.permissions"],
772
+ });
773
+ ```
774
+
775
+ ---
776
+
777
+ ## 📊 Aggregation Queries
778
+
779
+ Run aggregate queries directly on the repository or query builder.
780
+
781
+ ```typescript
782
+ // Repository-level aggregates
783
+ const total = await userRepository.count();
784
+ const exists = await userRepository.exists({
785
+ email: "admin@offbytesecure.com",
786
+ });
787
+
788
+ const stats = await userRepository.aggregate({
789
+ count: "*",
790
+ sum: ["salary"],
791
+ avg: ["salary"],
792
+ min: ["salary"],
793
+ max: ["salary"],
794
+ });
795
+ // stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }
796
+ ```
797
+
798
+ ---
799
+
800
+ ## 🔎 findOneBy / findBy
801
+
802
+ TypeORM-style conditional finders without writing raw SQL.
803
+
804
+ ```typescript
805
+ // Find one record by condition
806
+ const user = await userRepository.findOneBy({ email: "lwazicd@icloud.com" });
807
+
808
+ // Find multiple records
809
+ const admins = await userRepository.findBy(
810
+ { role: "admin" },
811
+ { limit: 10, orderBy: "createdAt DESC" },
812
+ );
813
+
814
+ // Combined with relations
815
+ const user = await userRepository.findOneBy(
816
+ { email: "lwazicd@icloud.com" },
817
+ { relations: ["roles"] },
818
+ );
819
+ ```
820
+
821
+ ---
822
+
823
+ ## 🔢 findAndCount
824
+
825
+ Get paginated results with a total count in a single call.
826
+
827
+ ```typescript
828
+ const { data, total } = await userRepository.findAndCount();
829
+ console.log(`Showing ${data.length} of ${total} total records`);
830
+ ```
831
+
832
+ ---
833
+
834
+ ## 🖱️ Cursor-Based Pagination
835
+
836
+ Efficient cursor-based pagination for large datasets.
837
+
838
+ ```typescript
839
+ // First page
840
+ const page1 = await userRepository.findMany({
841
+ take: 10,
842
+ orderBy: { field: "id", direction: "ASC" },
843
+ });
844
+
845
+ // Next page using cursor
846
+ const lastId = page1[page1.length - 1].id;
847
+ const page2 = await userRepository.findMany({
848
+ cursor: { field: "id", value: lastId, direction: "forward" },
849
+ take: 10,
850
+ orderBy: { field: "id", direction: "ASC" },
851
+ });
852
+ ```
853
+
854
+ ---
855
+
856
+ ## 🌱 Database Seeding
857
+
858
+ Define and run seeds for populating development/test databases.
859
+
860
+ ```typescript
861
+ import { defineSeed, runSeeds, resetDatabase } from "stabilize-orm";
862
+
863
+ // Define a seed
864
+ defineSeed("create-default-roles", async (db) => {
865
+ const roleRepo = orm.getRepository(Role);
866
+ await roleRepo.bulkCreate([
867
+ { name: "Admin", permissions: "all" },
868
+ { name: "User", permissions: "read" },
869
+ ]);
870
+ });
871
+
872
+ // Run all seeds
873
+ await runSeeds(orm.client);
874
+
875
+ // Reset database (drop, migrate, seed)
876
+ await resetDatabase(orm.client, [User, Role]);
877
+ ```
878
+
879
+ ---
880
+
881
+ ## 🏥 Health Check
882
+
883
+ Monitor database and cache connectivity with latency.
884
+
885
+ ```typescript
886
+ const health = await orm.healthCheck();
887
+ // { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }
888
+
889
+ // Per-table health
890
+ const userHealth = await userRepository.healthCheck();
891
+ // { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }
892
+ ```
893
+
894
+ ---
895
+
896
+ ## 📂 Bulk Upsert
897
+
898
+ Upsert multiple records in a single transaction.
899
+
900
+ ```typescript
901
+ const users = await userRepository.bulkUpsert(
902
+ [
903
+ { email: "lwazicd@icloud.com", name: "Lwazi" },
904
+ { email: "ciniso@icloud.com", name: "Ciniso" },
905
+ ],
906
+ ["email"], // unique key(s)
907
+ );
908
+ ```
909
+
910
+ ---
911
+
912
+ ## 🔧 Raw SQL
913
+
914
+ Execute raw SQL queries directly from the ORM instance.
915
+
916
+ ```typescript
917
+ const results = await orm.rawQuery("SELECT * FROM users WHERE age > ?", [25]);
918
+ const { affectedRows } = await orm.rawExec(
919
+ "UPDATE users SET active = false WHERE last_login < ?",
920
+ [oneYearAgo],
921
+ );
922
+ ```
923
+
924
+ ---
925
+
926
+ ## 🗑️ recoverAll / truncate
927
+
928
+ ```typescript
929
+ // Restore all soft-deleted records
930
+ const recovered = await userRepository.recoverAll();
931
+ console.log(`Recovered ${recovered} records`);
932
+
933
+ // Clear the table
934
+ await userRepository.truncate();
935
+ ```
936
+
937
+ ---
938
+
939
+ ## ⬆️⬇️ Increment / Decrement
940
+
941
+ Atomically update numeric fields without loading the record.
942
+
943
+ ```typescript
944
+ const updated = await userRepository.increment(user.id, "loginCount", 1);
945
+ const updated = await userRepository.decrement(user.id, "credits", 5);
946
+ ```
947
+
948
+ ---
949
+
950
+ ## 🏷️ Pluck / SelectColumns
951
+
952
+ ```typescript
953
+ // Get just the email column as an array
954
+ const emails = await userRepository.pluck("email");
955
+ // ["lwazicd@icloud.com", "ciniso@icloud.com", ...]
956
+
957
+ // Get specific columns
958
+ const users = await userRepository.selectColumns("id", "email");
959
+ // [{ id: 1, email: "lwazicd@icloud.com" }, ...]
960
+ ```
961
+
962
+ ---
963
+
964
+ ## 🔄 Toggle
965
+
966
+ Toggle a boolean field.
967
+
968
+ ```typescript
969
+ const toggled = await userRepository.toggle(user.id, "isActive");
970
+ // isActive was true, now false (or vice versa)
971
+ ```
972
+
973
+ ---
974
+
975
+ ## ✏️ updateBy / deleteBy
976
+
977
+ ```typescript
978
+ // Update all active users' role to "member"
979
+ const updated = await userRepository.updateBy(
980
+ { isActive: true },
981
+ { role: "member" },
982
+ );
983
+
984
+ // Delete all users with null email
985
+ const deleted = await userRepository.deleteBy({ email: null });
986
+ ```
987
+
988
+ ---
989
+
990
+ ## 🕳️ findDeleted / withTrashed
991
+
992
+ ```typescript
993
+ // Get only soft-deleted records
994
+ const deletedUsers = await userRepository.findDeleted().execute();
995
+
996
+ // Get all records including soft-deleted
997
+ const allUsers = await userRepository.withTrashed().execute();
998
+
999
+ // Restore matching soft-deleted records
1000
+ const restored = await userRepository.restoreBy({ role: "admin" });
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ## 🔄 firstOrCreate / updateOrCreate
1006
+
1007
+ ```typescript
1008
+ // Find or create in one call
1009
+ const user = await userRepository.firstOrCreate(
1010
+ { email: "lwazicd@icloud.com" },
1011
+ { name: "Lwazi" },
1012
+ );
1013
+
1014
+ // Find and update, or create if not found
1015
+ const user = await userRepository.updateOrCreate(
1016
+ { email: "lwazicd@icloud.com" },
1017
+ { name: "Updated Name" },
1018
+ );
1019
+ ```
1020
+
1021
+ ---
1022
+
1023
+ ## 🥇 first / last / random
1024
+
1025
+ ```typescript
1026
+ const firstUser = await userRepository.first();
1027
+ const admin = await userRepository.first({ role: "admin" });
1028
+ const lastUser = await userRepository.last();
1029
+ const randomUser = await userRepository.random();
1030
+ ```
1031
+
1032
+ ---
1033
+
1034
+ ## 🔒 Pessimistic Locking (lockForUpdate)
1035
+
1036
+ ```typescript
1037
+ const user = await userRepository.lockForUpdate(user.id);
1038
+ // The row is now locked for the duration of the transaction
1039
+ ```
1040
+
1041
+ ---
1042
+
1043
+ ## 📡 Event Emitter
1044
+
1045
+ Subscribe to ORM lifecycle events.
1046
+
1047
+ ```typescript
1048
+ const orm = new Stabilize(dbConfig);
1049
+
1050
+ orm.events.on("query", (entry) => {
1051
+ console.log(`[${entry.durationMs}ms] ${entry.query}`);
1052
+ });
1053
+
1054
+ orm.events.on("error", (err) => {
1055
+ console.error("ORM error:", err);
1056
+ });
1057
+
1058
+ orm.events.on("connection:open", (dbType) => {
1059
+ console.log(`Connected to ${dbType}`);
1060
+ });
1061
+ ```
1062
+
1063
+ ---
1064
+
1065
+ ## 📦 Pool Stats
1066
+
1067
+ ```typescript
1068
+ const stats = await orm.poolStats();
1069
+ // { active: 5, idle: 10, total: 15 }
1070
+ ```
1071
+
1072
+ ---
1073
+
1074
+ ## 🆔 generateUUID
1075
+
1076
+ ```typescript
1077
+ import { generateUUID } from "stabilize-orm";
1078
+
1079
+ const id = generateUUID();
1080
+ // "550e8400-e29b-41d4-a716-446655440000"
1081
+ ```
1082
+
1083
+ ---
1084
+
1085
+ ## 📑 License
1086
+
1087
+ Licensed under the MIT License. See [LICENSE.md](./LICENSE.md) for details.
1088
+
1089
+ ---
1090
+
1091
+ <div align="center">
1092
+
1093
+ Created with ❤️ by **ElectronSz**
1094
+ <br/>
1095
+ <em>File last updated: 2026-04-02</em>
1096
+
1097
+ </div>