stabilize-orm 1.3.8 → 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 (75) hide show
  1. package/README.md +1097 -546
  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 +104 -25
  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/query-builder.ts +0 -209
  72. package/repository.ts +0 -1096
  73. package/tests/migrations.test.ts +0 -141
  74. package/tsconfig.json +0 -32
  75. package/types.ts +0 -106
package/README.md CHANGED
@@ -1,546 +1,1097 @@
1
- # Stabilize ORM
2
-
3
- _A Modern, Type-Safe, and Expressive ORM for Bun_
4
-
5
- ---
6
-
7
- **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.
8
-
9
- ---
10
-
11
- ## 🚀 Features
12
-
13
- - **Unified API**: Write once, run on PostgreSQL, MySQL, or SQLite.
14
- - **Programmatic Model Definitions**: Define models and columns using the `defineModel` API with the `DataTypes` enum for database-agnostic schemas.
15
- - **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).
16
- - **Automatic Migrations**: Generate database-specific SQL schemas directly from your model definitions.
17
- - **Versioned Models & Time-Travel**: Enable versioning in your model configuration for automatic history tables and snapshot queries.
18
- - **Retry Logic**: Automatic exponential backoff for database queries to handle transient connection issues.
19
- - **Connection Pooling**: Efficient connection management for PostgreSQL and MySQL.
20
- - **Transactional Integrity**: Built-in support for atomic transactions with automatic rollback on failure.
21
- - **Advanced Query Builder**: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
22
- - **Model Relationships**: Define `OneToOne`, `ManyToOne`, `OneToMany`, and `ManyToMany` relationships in the model configuration.
23
- - **Soft Deletes**: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
24
- - **Lifecycle Hooks**: Define hooks in the model configuration or as class methods for lifecycle events like `beforeCreate`, `afterUpdate`, etc.
25
- - **Pluggable Logging**: Includes a robust `StabilizeLogger` with support for file-based, rotating logs.
26
- - **Custom Errors**: `StabilizeError` provides clear, consistent error handling.
27
- - **Caching Layer**: Optional Redis-backed caching with `cache-aside` and `write-through` strategies.
28
- - **Custom Query Scopes**: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
29
- - **Timestamps**: Automatically manage `createdAt` and `updatedAt` columns for tracking record creation and update times.
30
-
31
- ---
32
-
33
- ## 📦 Installation
34
-
35
- Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).
36
-
37
- ```bash
38
- # Using Bun
39
- bun add stabilize-orm
40
-
41
- # Using npm
42
- npm install stabilize-orm
43
- ```
44
-
45
- ---
46
-
47
- ## 📃 Documentation & Community
48
-
49
- - [Changelog](./CHANGELOG.md)
50
- - [License](./LICENSE.md)
51
- - [Code of Conduct](./CODE_OF_CONDUCT.md)
52
- - [Contributing Guide](./CONTRIBUTING.md)
53
- - [Security Policy](./SECURITY.md)
54
- - [Support](./SUPPORT.md)
55
- - [Funding](./FUNDING.md)
56
-
57
- ---
58
-
59
- ## ⚙️ Configuration
60
-
61
- Create a database configuration file.
62
-
63
- ```typescript
64
- // config/database.ts
65
- import { DBType, type DBConfig } from "stabilize-orm";
66
-
67
- const dbConfig: DBConfig = {
68
- type: DBType.Postgres,
69
- connectionString: process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
70
- retryAttempts: 3,
71
- retryDelay: 1000,
72
- };
73
-
74
- export default dbConfig;
75
- ```
76
-
77
- Next, create a central ORM instance for your application.
78
-
79
- ```typescript
80
- // db.ts
81
- import { Stabilize, type CacheConfig, type LoggerConfig, LogLevel } from "stabilize-orm";
82
- import dbConfig from "./database";
83
-
84
- const cacheConfig: CacheConfig = {
85
- enabled: process.env.CACHE_ENABLED === "true",
86
- redisUrl: process.env.REDIS_URL,
87
- ttl: 60,
88
- };
89
-
90
- const loggerConfig: LoggerConfig = {
91
- level: LogLevel.Info,
92
- filePath: "logs/stabilize.log",
93
- maxFileSize: 5 * 1024 * 1024, // 5MB
94
- maxFiles: 3,
95
- };
96
-
97
- export const orm = new Stabilize(dbConfig, cacheConfig, loggerConfig);
98
- ```
99
-
100
- ---
101
-
102
- ## 🏗️ Models & Relationships
103
-
104
- Define your tables as classes using the `defineModel` function. The `DataTypes` enum ensures database-agnostic schemas.
105
-
106
- ### Example: Users and Roles (Many-to-Many) with Versioning
107
-
108
- ```typescript
109
- // models/User.ts
110
- import { defineModel, DataTypes, RelationType } from "stabilize-orm";
111
- import { UserRole } from "./UserRole";
112
-
113
- const User = defineModel({
114
- tableName: "users",
115
- versioned: true,
116
- columns: {
117
- id: { type: DataTypes.Integer, required: true },
118
- email: { type: DataTypes.String, length: 100, required: true, unique: true },
119
- },
120
- relations: [
121
- {
122
- type: RelationType.OneToMany,
123
- target: () => UserRole,
124
- property: "roles",
125
- foreignKey: "userId",
126
- },
127
- ],
128
- hooks: {
129
- beforeCreate: (entity) => console.log(`Creating user: ${entity.email}`),
130
- },
131
- });
132
-
133
- // Add a hook as a class method
134
- User.prototype.afterCreate = async function () {
135
- console.log(`Created user with ID: ${this.id}`);
136
- };
137
-
138
- export { User };
139
- ```
140
-
141
- ```typescript
142
- // models/Role.ts
143
- import { defineModel, DataTypes } from "stabilize-orm";
144
-
145
- const Role = defineModel({
146
- tableName: "roles",
147
- columns: {
148
- id: { type: DataTypes.Integer, required: true },
149
- name: { type: DataTypes.String, length: 50, required: true, unique: true },
150
- },
151
- });
152
-
153
- export { Role };
154
- ```
155
-
156
- ```typescript
157
- // models/UserRole.ts
158
- import { defineModel, DataTypes, RelationType } from "stabilize-orm";
159
- import { User } from "./User";
160
- import { Role } from "./Role";
161
-
162
- const UserRole = defineModel({
163
- tableName: "user_roles",
164
- columns: {
165
- id: { type: DataTypes.Integer, required: true },
166
- userId: { type: DataTypes.Integer, required: true, index: "idx_user_id" },
167
- roleId: { type: DataTypes.Integer, required: true, index: "idx_role_id" },
168
- },
169
- relations: [
170
- {
171
- type: RelationType.ManyToOne,
172
- target: () => User,
173
- property: "user",
174
- foreignKey: "userId",
175
- },
176
- {
177
- type: RelationType.ManyToOne,
178
- target: () => Role,
179
- property: "role",
180
- foreignKey: "roleId",
181
- },
182
- ],
183
- });
184
-
185
- export { UserRole };
186
- ```
187
-
188
- ---
189
-
190
- ## ⏳ Versioning & Auditing
191
-
192
- Enable automatic history tracking and time-travel queries by setting `versioned: true` in your model configuration.
193
-
194
- - Each change is recorded in a `<table>_history` table with version, operation, and audit columns.
195
- - Supports snapshot queries, rollbacks, audits, and time-travel.
196
-
197
- ### **Versioning Example**
198
-
199
- ```typescript
200
- import { defineModel, DataTypes } from "stabilize-orm";
201
-
202
- const User = defineModel({
203
- tableName: "users",
204
- versioned: true,
205
- columns: {
206
- id: { type: DataTypes.Integer, required: true },
207
- name: { type: DataTypes.String, length: 100 },
208
- },
209
- });
210
-
211
- // --- Using versioning features:
212
-
213
- const userRepository = orm.getRepository(User);
214
-
215
- // Rollback to a previous version
216
- await userRepository.rollback(1, 3); // roll back user with id=1 to version 3
217
-
218
- // Get a snapshot as of a specific date
219
- const userAsOf = await userRepository.asOf(1, new Date("2025-01-01T00:00:00Z"));
220
- console.log(userAsOf);
221
-
222
- // View full version history
223
- const history = await userRepository.history(1);
224
- console.log(history);
225
- ```
226
-
227
- ---
228
-
229
- ## 🔄 Model Lifecycle Hooks
230
-
231
- 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.
232
-
233
- ### **Hooks Example**
234
-
235
- ```typescript
236
- import { defineModel, DataTypes } from "stabilize-orm";
237
-
238
- const User = defineModel({
239
- tableName: "users",
240
- columns: {
241
- id: { type: DataTypes.Integer, required: true },
242
- name: { type: DataTypes.String, length: 100 },
243
- createdAt: { type: DataTypes.DateTime },
244
- updatedAt: { type: DataTypes.DateTime },
245
- },
246
- hooks: {
247
- beforeCreate: (entity) => {
248
- entity.createdAt = new Date();
249
- },
250
- beforeUpdate: (entity) => {
251
- entity.updatedAt = new Date();
252
- },
253
- afterCreate: (entity) => {
254
- console.log(`User created: ${entity.name}`);
255
- },
256
- },
257
- });
258
-
259
- // Add a hook as a class method
260
- User.prototype.afterUpdate = async function () {
261
- console.log(`Updated user: ${this.name}`);
262
- };
263
-
264
- export { User };
265
- ```
266
-
267
- Supported hooks: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `beforeSave`, `afterSave`.
268
-
269
- ---
270
-
271
- ## 💻 Command-Line Interface (CLI)
272
-
273
- Stabilize includes a powerful CLI for managing your workflow. See: [stabilize-cli on GitHub](https://github.com/ElectronSz/stabilize-cli)
274
-
275
- ### Generating Files
276
-
277
- - **Generate a model**:
278
- ```bash
279
- stabilize-cli generate model Product
280
- ```
281
-
282
- - **Generate a migration from a model**:
283
- ```bash
284
- stabilize-cli generate migration User
285
- ```
286
-
287
- - **Generate a seed file**:
288
- ```bash
289
- stabilize-cli generate seed InitialRoles
290
- ```
291
-
292
- ### Database & Migration Management
293
-
294
- - **Run all pending migrations**:
295
- ```bash
296
- stabilize-cli migrate
297
- ```
298
-
299
- - **Roll back the last migration**:
300
- ```bash
301
- stabilize-cli migrate:rollback
302
- ```
303
-
304
- - **Run all pending seeds (in dependency order)**:
305
- ```bash
306
- stabilize-cli seed
307
- ```
308
-
309
- - **Check the status of migrations and seeds**:
310
- ```bash
311
- stabilize-cli status
312
- ```
313
-
314
- - **Reset the database (drop, migrate, seed)**:
315
- ```bash
316
- stabilize-cli db:reset
317
- ```
318
-
319
- ---
320
-
321
- ## 🧑‍💻 Querying Data
322
-
323
- ### Basic CRUD with Repositories
324
-
325
- ```typescript
326
- import { orm } from "./db";
327
- import { User } from "./models/User";
328
-
329
- const userRepository = orm.getRepository(User);
330
-
331
- const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
332
- const foundUser = await userRepository.findOne(newUser.id);
333
- const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
334
- await userRepository.delete(newUser.id);
335
- ```
336
-
337
- ### Advanced Queries with the Query Builder
338
-
339
- ```typescript
340
- const activeAdmins = await orm
341
- .getRepository(UserRole)
342
- .find()
343
- .join("users", "user_roles.user_id = users.id")
344
- .join("roles", "user_roles.role_id = roles.id")
345
- .select("users.id", "users.email", "roles.name as role_name")
346
- .where("roles.name = ?", "Admin")
347
- .orderBy("users.email ASC")
348
- .execute();
349
-
350
- console.log(activeAdmins);
351
- ```
352
-
353
- #### Query Builder API
354
-
355
- ```typescript
356
- {
357
- select(...fields: string[]): QueryBuilder<User>;
358
- where(condition: string, ...params: any[]): QueryBuilder<User>;
359
- join(table: string, condition: string): QueryBuilder<User>;
360
- orderBy(clause: string): QueryBuilder<User>;
361
- limit(limit: number): QueryBuilder<User>;
362
- offset(offset: number): QueryBuilder<User>;
363
- scope(name: string, ...args: any[]): QueryBuilder<User>;
364
- build(): { query: string; params: any[] };
365
- execute(client?: DBClient, cache?: Cache, cacheKey?: string): Promise<User[]>;
366
- }
367
- ```
368
-
369
- ### Custom Query Scopes
370
-
371
- 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.
372
-
373
- #### **Scopes Example**
374
-
375
- ```typescript
376
- import { defineModel, DataTypes } from "stabilize-orm";
377
- import { orm } from "./db";
378
-
379
- const User = defineModel({
380
- tableName: "users",
381
- columns: {
382
- id: { type: DataTypes.Integer, required: true },
383
- email: { type: DataTypes.String, length: 100, required: true },
384
- isActive: { type: DataTypes.Boolean, required: true },
385
- createdAt: { type: DataTypes.DateTime },
386
- updatedAt: { type: DataTypes.DateTime },
387
- },
388
- scopes: {
389
- active: (qb) => qb.where("isActive = ?", true),
390
- recent: (qb, days: number) => qb.where("createdAt >= ?", new Date(Date.now() - days * 24 * 60 * 60 * 1000)),
391
- },
392
- });
393
-
394
- const userRepository = orm.getRepository(User);
395
-
396
- // Fetch active users
397
- const activeUsers = await userRepository.scope("active").execute();
398
-
399
- // Fetch users created in the last 7 days
400
- const recentUsers = await userRepository.scope("recent", 7).execute();
401
-
402
- // Combine scopes with other query operations
403
- const recentActiveUsers = await userRepository
404
- .scope("active")
405
- .scope("recent", 7)
406
- .orderBy("createdAt DESC")
407
- .limit(10)
408
- .execute();
409
-
410
- console.log(recentActiveUsers);
411
- ```
412
-
413
- ### Timestamps
414
-
415
- 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.
416
-
417
- #### **Timestamps Example**
418
-
419
- ```typescript
420
- import { defineModel, DataTypes } from "stabilize-orm";
421
- import { orm } from "./db";
422
-
423
- const User = defineModel({
424
- tableName: "users",
425
- columns: {
426
- id: { type: DataTypes.Integer, required: true },
427
- email: { type: DataTypes.String, length: 100, required: true },
428
- createdAt: { type: DataTypes.DateTime },
429
- updatedAt: { type: DataTypes.DateTime },
430
- },
431
- timestamps: {
432
- createdAt: "createdAt",
433
- updatedAt: "updatedAt",
434
- },
435
- });
436
-
437
- const userRepository = orm.getRepository(User);
438
-
439
- // Create a user (createdAt and updatedAt set automatically)
440
- const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
441
- console.log(newUser.createdAt, newUser.updatedAt); // Outputs current timestamp
442
-
443
- // Update a user (updatedAt updated automatically)
444
- const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
445
- console.log(updatedUser.updatedAt); // Outputs new timestamp
446
-
447
- // Bulk create users
448
- const newUsers = await userRepository.bulkCreate([
449
- { email: "user1@example.com" },
450
- { email: "user2@example.com" },
451
- ]);
452
- console.log(newUsers.map(u => u.createdAt)); // Outputs timestamps for each user
453
- ```
454
-
455
- ---
456
-
457
- ## 🗑️ Soft Deletes
458
-
459
- Enable soft deletes by setting `softDelete: true` and marking a column (e.g., `deletedAt`) with `softDelete: true` in the model configuration.
460
-
461
- - Use `repository.delete(id)` to mark an entity as deleted.
462
- - Use `repository.recover(id)` to restore a soft-deleted entity.
463
- - Queries automatically exclude soft-deleted rows unless specified otherwise.
464
-
465
- ### **Soft Delete Example**
466
-
467
- ```typescript
468
- import { defineModel, DataTypes } from "stabilize-orm";
469
-
470
- const User = defineModel({
471
- tableName: "users",
472
- softDelete: true,
473
- columns: {
474
- id: { type: DataTypes.Integer, required: true },
475
- email: { type: DataTypes.String, length: 100, required: true },
476
- deletedAt: { type: DataTypes.DateTime, softDelete: true },
477
- },
478
- });
479
-
480
- const userRepository = orm.getRepository(User);
481
- await userRepository.create({ email: "lwazicd@icloud.com" });
482
- await userRepository.delete(1); // Soft delete
483
- await userRepository.recover(1); // Recover
484
- ```
485
-
486
- ---
487
-
488
- ## 🌐 Express.js Integration
489
-
490
- Stabilize ORM works seamlessly with web frameworks like Express.
491
-
492
- ```typescript
493
- import express from "express";
494
- import { orm } from "./db";
495
- import { User } from "./models/User";
496
-
497
- const app = express();
498
- app.use(express.json());
499
-
500
- const userRepository = orm.getRepository(User);
501
-
502
- app.get("/users", async (req, res) => {
503
- try {
504
- const users = await userRepository.find().execute();
505
- res.json(users);
506
- } catch (err) {
507
- res.status(500).json({ error: "Failed to fetch users." });
508
- }
509
- });
510
-
511
- app.post("/users", async (req, res) => {
512
- try {
513
- const user = await userRepository.create(req.body);
514
- res.status(201).json(user);
515
- } catch (err) {
516
- res.status(500).json({ error: "User creation failed." });
517
- }
518
- });
519
-
520
- app.listen(3000, () => {
521
- console.log("Server listening on port 3000");
522
- });
523
- ```
524
-
525
- ---
526
-
527
- ## 🧑‍🔬 Testing & Time-Travel
528
-
529
- - Use time-travel queries to inspect historical entity states.
530
- - Assert audit trails and rollback operations in your tests.
531
-
532
- ---
533
-
534
- ## 📑 License
535
-
536
- Licensed under the MIT License. See [LICENSE.md](./LICENSE.md) for details.
537
-
538
- ---
539
-
540
- <div align="center">
541
-
542
- Created with ❤️ by **ElectronSz**
543
- <br/>
544
- <em>File last updated: 2025-10-19 11:12:00 SAST</em>
545
-
546
- </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>