stabilize-orm 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ - Further features and improvements coming soon.
8
+
9
+ ## [1.3.0] - 2025-10-18
10
+
11
+ ### Added
12
+ - Introduced programmatic `defineModel` API for model definitions, eliminating the need for decorators (`model.ts`).
13
+ - Added `MetadataStorage` class to manage model configurations without `reflect-metadata`.
14
+ - Added support for defining lifecycle hooks in `ModelConfig` or as class methods (`hooks.ts`).
15
+ - Added `example.ts` to demonstrate the new programmatic API usage.
16
+ - Extended `ModelConfig` interface to support columns, relations, hooks, versioning, and soft deletes (`types.ts`).
17
+
18
+ ### Changed
19
+ - Replaced decorator-based model definitions with `defineModel` API, removing dependency on `reflect-metadata` and TypeScript experimental features (`experimentalDecorators`, `emitDecoratorMetadata`).
20
+ - Updated `stabilize.ts` to export `defineModel` and remove `reflect-metadata` import.
21
+ - Modified `repository.ts` to use `MetadataStorage` for table names, columns, relations, validators, and soft delete fields.
22
+ - Rewrote `hooks.ts` to support hooks via `ModelConfig` and class methods, integrated with `MetadataStorage`.
23
+ - Updated `migrations.ts` to generate schemas using `MetadataStorage` instead of decorator metadata.
24
+ - Revised `types.ts` to remove decorator-related types and add `ModelConfig`, `ColumnConfig`, and `RelationConfig` interfaces.
25
+ - Updated `README.md` to reflect the new programmatic API, remove decorator references, and update examples.
26
+ - Ensured compatibility with `verbatimModuleSyntax` by using `export type` for type exports in `stabilize.ts`.
27
+
28
+ ### Removed
29
+ - Deleted `decorators.ts` as decorators are no longer used.
30
+ - Removed dependency on `reflect-metadata` from the project.
31
+
32
+ ### Fixed
33
+ - Fixed TypeScript type errors in `repository.ts` for `columns` and `relations` by mapping `MetadataStorage` outputs to match expected types.
34
+ - Corrected `runHooks` in `repository.ts` to call `hook.callback(entity)` instead of `hook()`.
35
+ - Fixed TypeScript `verbatimModuleSyntax` error in `stabilize.ts` by separating type and value exports.
36
+
5
37
  ## [1.1.2] - 2025-10-14
6
38
 
7
39
  ### Added
@@ -16,8 +48,4 @@ All notable changes to this project will be documented in this file.
16
48
  - Enhanced documentation for open source best practices.
17
49
 
18
50
  ### Fixed
19
- - Various bug fixes for connection handling and retry logic.
20
-
21
- ## [Unreleased]
22
-
23
- - Further features and improvements coming soon.
51
+ - Various bug fixes for connection handling and retry logic.
package/README.md CHANGED
@@ -1,27 +1,27 @@
1
1
  # Stabilize ORM
2
2
 
3
- _A Modern, Type-Safe, and Expressive ORM for Bun, Node.js, and Deno_
3
+ _A Modern, Type-Safe, and Expressive ORM for Bun_
4
4
 
5
5
  ---
6
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, elegant decorator-based models, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.
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
8
 
9
9
  ---
10
10
 
11
11
  ## 🚀 Features
12
12
 
13
13
  - **Unified API**: Write once, run on PostgreSQL, MySQL, or SQLite.
14
- - **Type-Safe Decorators**: Define models and columns with the powerful `DataTypes` enum for true database-agnostic schemas.
15
- - **Full-Featured CLI**: Generate models, manage migrations, seed data, and reset your database from the command line.
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
16
  - **Automatic Migrations**: Generate database-specific SQL schemas directly from your model definitions.
17
- - **Versioned Models & Time-Travel**: Add `@Versioned()` to your models for automatic history tables and snapshot queries.
17
+ - **Versioned Models & Time-Travel**: Enable versioning in your model configuration for automatic history tables and snapshot queries.
18
18
  - **Retry Logic**: Automatic exponential backoff for database queries to handle transient connection issues.
19
19
  - **Connection Pooling**: Efficient connection management for PostgreSQL and MySQL.
20
20
  - **Transactional Integrity**: Built-in support for atomic transactions with automatic rollback on failure.
21
21
  - **Advanced Query Builder**: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
22
- - **Model Relationships**: Use `OneToOne`, `ManyToOne`, `OneToMany`, and `ManyToMany` decorators for relationships.
23
- - **Soft Deletes**: Add `@SoftDelete()` to your model for transparent "deleted" flags and safe row removal.
24
- - **Lifecycle Hooks**: Use the `@Hook()` decorator for model lifecycle events like `beforeCreate`, `afterUpdate`, etc.
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
25
  - **Pluggable Logging**: Includes a robust `ConsoleLogger` with support for file-based, rotating logs.
26
26
  - **Custom Errors**: `StabilizeError` provides clear, consistent error handling.
27
27
  - **Caching Layer**: Optional Redis-backed caching with `cache-aside` and `write-through` strategies.
@@ -30,14 +30,14 @@ _A Modern, Type-Safe, and Expressive ORM for Bun, Node.js, and Deno_
30
30
 
31
31
  ## 📦 Installation
32
32
 
33
- Stabilize ORM requires a modern JavaScript runtime (Bun v1.0+, Node.js v18+, Deno v1.28+).
33
+ Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).
34
34
 
35
35
  ```bash
36
36
  # Using Bun
37
- bun add stabilize-orm reflect-metadata
37
+ bun add stabilize-orm
38
38
 
39
39
  # Using npm
40
- npm install stabilize-orm reflect-metadata
40
+ npm install stabilize-orm
41
41
  ```
42
42
 
43
43
  ---
@@ -56,14 +56,14 @@ npm install stabilize-orm reflect-metadata
56
56
 
57
57
  ## ⚙️ Configuration
58
58
 
59
- First, create a database configuration file.
59
+ Create a database configuration file.
60
60
 
61
61
  ```typescript
62
62
  // config/database.ts
63
63
  import { DBType, type DBConfig } from "stabilize-orm";
64
64
 
65
65
  const dbConfig: DBConfig = {
66
- type: DBType.Postgres,
66
+ type: DBType.Postgres,
67
67
  connectionString: process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
68
68
  retryAttempts: 3,
69
69
  retryDelay: 1000,
@@ -72,11 +72,10 @@ const dbConfig: DBConfig = {
72
72
  export default dbConfig;
73
73
  ```
74
74
 
75
- Next, create a central ORM instance that your application can use. Import `reflect-metadata` once at your application's entry point.
75
+ Next, create a central ORM instance for your application.
76
76
 
77
77
  ```typescript
78
78
  // db.ts
79
- import 'reflect-metadata';
80
79
  import { Stabilize, type CacheConfig, type LoggerConfig, LogLevel } from "stabilize-orm";
81
80
  import dbConfig from "./database";
82
81
 
@@ -88,7 +87,7 @@ const cacheConfig: CacheConfig = {
88
87
 
89
88
  const loggerConfig: LoggerConfig = {
90
89
  level: LogLevel.Info,
91
- filePath: 'logs/stabilize.log',
90
+ filePath: "logs/stabilize.log",
92
91
  maxFileSize: 5 * 1024 * 1024, // 5MB
93
92
  maxFiles: 3,
94
93
  };
@@ -100,80 +99,95 @@ export const orm = new Stabilize(dbConfig, cacheConfig, loggerConfig);
100
99
 
101
100
  ## 🏗️ Models & Relationships
102
101
 
103
- Define your tables as classes using decorators. The `@Column` decorator uses the `DataTypes` enum for a truly database-agnostic schema.
102
+ Define your tables as classes using the `defineModel` function. The `DataTypes` enum ensures database-agnostic schemas.
104
103
 
105
104
  ### Example: Users and Roles (Many-to-Many) with Versioning
106
105
 
107
106
  ```typescript
108
107
  // models/User.ts
109
- import 'reflect-metadata';
110
- import { Model, Column, DataTypes, Required, Unique, OneToMany, Versioned } from 'stabilize-orm';
111
- import { UserRole } from './UserRole';
112
-
113
- @Model('users')
114
- @Versioned() // Enables history table for time-travel/audit
115
- export class User {
116
- @Column({ type: DataTypes.INTEGER, name: 'id' })
117
- id!: number;
118
-
119
- @Column({ type: DataTypes.STRING, length: 100 })
120
- @Required() @Unique()
121
- email!: string;
122
-
123
- @OneToMany(() => UserRole, 'user')
124
- roles?: UserRole[];
125
- }
108
+ import { defineModel, DataTypes, RelationType } from "stabilize-orm";
109
+ import { UserRole } from "./UserRole";
110
+
111
+ const User = defineModel({
112
+ tableName: "users",
113
+ versioned: true,
114
+ columns: {
115
+ id: { type: DataTypes.Integer, required: true },
116
+ email: { type: DataTypes.String, length: 100, required: true, unique: true },
117
+ },
118
+ relations: [
119
+ {
120
+ type: RelationType.OneToMany,
121
+ target: () => UserRole,
122
+ property: "roles",
123
+ foreignKey: "userId",
124
+ },
125
+ ],
126
+ hooks: {
127
+ beforeCreate: (entity) => console.log(`Creating user: ${entity.email}`),
128
+ },
129
+ });
130
+
131
+ // Add a hook as a class method
132
+ User.prototype.afterCreate = async function () {
133
+ console.log(`Created user with ID: ${this.id}`);
134
+ };
135
+
136
+ export { User };
126
137
  ```
127
138
 
128
139
  ```typescript
129
140
  // models/Role.ts
130
- import 'reflect-metadata';
131
- import { Model, Column, DataTypes, Required, Unique } from 'stabilize-orm';
132
-
133
- @Model('roles')
134
- export class Role {
135
- @Column({ type: DataTypes.INTEGER, name: 'id' })
136
- id!: number;
141
+ import { defineModel, DataTypes } from "stabilize-orm";
142
+
143
+ const Role = defineModel({
144
+ tableName: "roles",
145
+ columns: {
146
+ id: { type: DataTypes.Integer, required: true },
147
+ name: { type: DataTypes.String, length: 50, required: true, unique: true },
148
+ },
149
+ });
137
150
 
138
- @Column({ type: DataTypes.STRING, length: 50 })
139
- @Required() @Unique()
140
- name!: string;
141
- }
151
+ export { Role };
142
152
  ```
143
153
 
144
154
  ```typescript
145
155
  // models/UserRole.ts
146
- import 'reflect-metadata';
147
- import { Model, Column, DataTypes, Required, ManyToOne, Index } from 'stabilize-orm';
148
- import { User } from './User';
149
- import { Role } from './Role';
150
-
151
- @Model('user_roles')
152
- export class UserRole {
153
- @Column({ type: DataTypes.INTEGER, name: 'id' })
154
- id!: number;
155
-
156
- @Column({ type: DataTypes.INTEGER, name: 'user_id' })
157
- @Required() @Index()
158
- userId!: number;
159
-
160
- @Column({ type: DataTypes.INTEGER, name: 'role_id' })
161
- @Required() @Index()
162
- roleId!: number;
163
-
164
- @ManyToOne(() => User, 'userId')
165
- user?: User;
156
+ import { defineModel, DataTypes, RelationType } from "stabilize-orm";
157
+ import { User } from "./User";
158
+ import { Role } from "./Role";
159
+
160
+ const UserRole = defineModel({
161
+ tableName: "user_roles",
162
+ columns: {
163
+ id: { type: DataTypes.Integer, required: true },
164
+ userId: { type: DataTypes.Integer, required: true, index: "idx_user_id" },
165
+ roleId: { type: DataTypes.Integer, required: true, index: "idx_role_id" },
166
+ },
167
+ relations: [
168
+ {
169
+ type: RelationType.ManyToOne,
170
+ target: () => User,
171
+ property: "user",
172
+ foreignKey: "userId",
173
+ },
174
+ {
175
+ type: RelationType.ManyToOne,
176
+ target: () => Role,
177
+ property: "role",
178
+ foreignKey: "roleId",
179
+ },
180
+ ],
181
+ });
166
182
 
167
- @ManyToOne(() => Role, 'roleId')
168
- role?: Role;
169
- }
183
+ export { UserRole };
170
184
  ```
171
185
 
172
186
  ---
173
187
 
174
188
  ## ⏳ Versioning & Auditing
175
189
 
176
- Enable automatic history tracking and time-travel queries by adding `@Versioned()` to your model.
190
+ Enable automatic history tracking and time-travel queries by setting `versioned: true` in your model configuration.
177
191
 
178
192
  - Each change is recorded in a `<table>_history` table with version, operation, and audit columns.
179
193
  - Supports snapshot queries, rollbacks, audits, and time-travel.
@@ -181,15 +195,16 @@ Enable automatic history tracking and time-travel queries by adding `@Versioned(
181
195
  ### **Versioning Example**
182
196
 
183
197
  ```typescript
184
- @Model('users')
185
- @Versioned()
186
- export class User {
187
- @Column({ type: DataTypes.INTEGER, name: 'id' })
188
- id!: number;
189
-
190
- @Column({ type: DataTypes.STRING, length: 100 })
191
- name!: string;
192
- }
198
+ import { defineModel, DataTypes } from "stabilize-orm";
199
+
200
+ const User = defineModel({
201
+ tableName: "users",
202
+ versioned: true,
203
+ columns: {
204
+ id: { type: DataTypes.Integer, required: true },
205
+ name: { type: DataTypes.String, length: 100 },
206
+ },
207
+ });
193
208
 
194
209
  // --- Using versioning features:
195
210
 
@@ -199,7 +214,7 @@ const userRepository = orm.getRepository(User);
199
214
  await userRepository.rollback(1, 3); // roll back user with id=1 to version 3
200
215
 
201
216
  // Get a snapshot as of a specific date
202
- const userAsOf = await userRepository.asOf(1, new Date('2025-01-01T00:00:00Z'));
217
+ const userAsOf = await userRepository.asOf(1, new Date("2025-01-01T00:00:00Z"));
203
218
  console.log(userAsOf);
204
219
 
205
220
  // View full version history
@@ -211,95 +226,92 @@ console.log(history);
211
226
 
212
227
  ## 🔄 Model Lifecycle Hooks
213
228
 
214
- Stabilize ORM supports lifecycle hooks via the `@Hook()` decorator.
215
- You can run logic before/after create, update, delete, or save.
229
+ 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.
216
230
 
217
231
  ### **Hooks Example**
218
232
 
219
233
  ```typescript
220
- import { Model, Column, DataTypes, Hook } from 'stabilize-orm';
221
-
222
- @Model('users')
223
- export class User {
224
- @Column({ type: DataTypes.INTEGER, name: 'id' })
225
- id!: number;
226
-
227
- @Column({ type: DataTypes.STRING, length: 100 })
228
- name!: string;
229
-
230
- @Column({ type: DataTypes.DATETIME, name: 'created_at' })
231
- createdAt!: Date;
232
-
233
- @Column({ type: DataTypes.DATETIME, name: 'updated_at' })
234
- updatedAt!: Date;
235
-
236
- @Hook('beforeCreate')
237
- setCreatedAt() {
238
- this.createdAt = new Date();
239
- }
234
+ import { defineModel, DataTypes } from "stabilize-orm";
235
+
236
+ const User = defineModel({
237
+ tableName: "users",
238
+ columns: {
239
+ id: { type: DataTypes.Integer, required: true },
240
+ name: { type: DataTypes.String, length: 100 },
241
+ createdAt: { type: DataTypes.DateTime },
242
+ updatedAt: { type: DataTypes.DateTime },
243
+ },
244
+ hooks: {
245
+ beforeCreate: (entity) => {
246
+ entity.createdAt = new Date();
247
+ },
248
+ beforeUpdate: (entity) => {
249
+ entity.updatedAt = new Date();
250
+ },
251
+ afterCreate: (entity) => {
252
+ console.log(`User created: ${entity.name}`);
253
+ },
254
+ },
255
+ });
240
256
 
241
- @Hook('beforeUpdate')
242
- setUpdatedAt() {
243
- this.updatedAt = new Date();
244
- }
257
+ // Add a hook as a class method
258
+ User.prototype.afterUpdate = async function () {
259
+ console.log(`Updated user: ${this.name}`);
260
+ };
245
261
 
246
- @Hook('afterCreate')
247
- logCreate() {
248
- console.log(`User created: ${this.name}`);
249
- }
250
- }
262
+ export { User };
251
263
  ```
252
264
 
253
- You can use `@Hook` with: `'beforeCreate'`, `'afterCreate'`, `'beforeUpdate'`, `'afterUpdate'`, `'beforeDelete'`, `'afterDelete'`, `'beforeSave'`, `'afterSave'`.
265
+ Supported hooks: `beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `beforeSave`, `afterSave`.
254
266
 
255
267
  ---
256
268
 
257
269
  ## 💻 Command-Line Interface (CLI)
258
270
 
259
- Stabilize includes a powerful CLI for managing your workflow.
271
+ Stabilize includes a powerful CLI for managing your workflow. See: [stabilize-cli on GitHub](https://github.com/ElectronSz/stabilize-cli)
260
272
 
261
273
  ### Generating Files
262
274
 
263
275
  - **Generate a model**:
264
276
  ```bash
265
- bun run stabilize-cli generate model Product
277
+ stabilize-cli generate model Product
266
278
  ```
267
279
 
268
280
  - **Generate a migration from a model**:
269
281
  ```bash
270
- bun run stabilize-cli generate migration User
282
+ stabilize-cli generate migration User
271
283
  ```
272
284
 
273
285
  - **Generate a seed file**:
274
286
  ```bash
275
- bun run stabilize-cli generate seed InitialRoles
287
+ stabilize-cli generate seed InitialRoles
276
288
  ```
277
289
 
278
290
  ### Database & Migration Management
279
291
 
280
292
  - **Run all pending migrations**:
281
293
  ```bash
282
- bun run stabilize-cli migrate
294
+ stabilize-cli migrate
283
295
  ```
284
296
 
285
297
  - **Roll back the last migration**:
286
298
  ```bash
287
- bun run stabilize-cli migrate:rollback
299
+ stabilize-cli migrate:rollback
288
300
  ```
289
301
 
290
302
  - **Run all pending seeds (in dependency order)**:
291
303
  ```bash
292
- bun run stabilize-cli seed
304
+ stabilize-cli seed
293
305
  ```
294
306
 
295
307
  - **Check the status of migrations and seeds**:
296
308
  ```bash
297
- bun run stabilize-cli status
309
+ stabilize-cli status
298
310
  ```
299
311
 
300
312
  - **Reset the database (drop, migrate, seed)**:
301
313
  ```bash
302
- bun run stabilize-cli db:reset
314
+ stabilize-cli db:reset
303
315
  ```
304
316
 
305
317
  ---
@@ -309,21 +321,22 @@ Stabilize includes a powerful CLI for managing your workflow.
309
321
  ### Basic CRUD with Repositories
310
322
 
311
323
  ```typescript
312
- import { orm } from './db';
313
- import { User } from 'models/User';
324
+ import { orm } from "./db";
325
+ import { User } from "./models/User";
314
326
 
315
327
  const userRepository = orm.getRepository(User);
316
328
 
317
- const newUser = await userRepository.create({ email: 'lwazicd@icloud.com' });
329
+ const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
318
330
  const foundUser = await userRepository.findOne(newUser.id);
319
- const updatedUser = await userRepository.update(newUser.id, { email: 'admin@offbytesecure.com' });
331
+ const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
320
332
  await userRepository.delete(newUser.id);
321
333
  ```
322
334
 
323
335
  ### Advanced Queries with the Query Builder
324
336
 
325
337
  ```typescript
326
- const activeAdmins = await orm.getRepository(UserRole)
338
+ const activeAdmins = await orm
339
+ .getRepository(UserRole)
327
340
  .find()
328
341
  .join("users", "user_roles.user_id = users.id")
329
342
  .join("roles", "user_roles.role_id = roles.id")
@@ -354,9 +367,32 @@ console.log(activeAdmins);
354
367
 
355
368
  ## 🗑️ Soft Deletes
356
369
 
357
- Add `@SoftDelete()` to a model property to enable transparent soft deletes (e.g., `deleted_at` timestamp).
358
- - Use `repository.softDelete(id)` to mark an entity as deleted.
359
- - Use `find({ includeDeleted: true })` to include soft-deleted rows.
370
+ Enable soft deletes by setting `softDelete: true` and marking a column (e.g., `deletedAt`) with `softDelete: true` in the model configuration.
371
+
372
+ - Use `repository.delete(id)` to mark an entity as deleted.
373
+ - Use `repository.recover(id)` to restore a soft-deleted entity.
374
+ - Queries automatically exclude soft-deleted rows unless specified otherwise.
375
+
376
+ ### **Soft Delete Example**
377
+
378
+ ```typescript
379
+ import { defineModel, DataTypes } from "stabilize-orm";
380
+
381
+ const User = defineModel({
382
+ tableName: "users",
383
+ softDelete: true,
384
+ columns: {
385
+ id: { type: DataTypes.Integer, required: true },
386
+ email: { type: DataTypes.String, length: 100, required: true },
387
+ deletedAt: { type: DataTypes.DateTime, softDelete: true },
388
+ },
389
+ });
390
+
391
+ const userRepository = orm.getRepository(User);
392
+ await userRepository.create({ email: "lwazicd@icloud.com" });
393
+ await userRepository.delete(1); // Soft delete
394
+ await userRepository.recover(1); // Recover
395
+ ```
360
396
 
361
397
  ---
362
398
 
@@ -416,6 +452,6 @@ Licensed under the MIT License. See [LICENSE.md](./LICENSE.md) for details.
416
452
 
417
453
  Created with ❤️ by **ElectronSz**
418
454
  <br/>
419
- <em>File last updated: 2025-10-16 19:41:00 UTC</em>
455
+ <em>File last updated: 2025-10-18 22:10:00 SAST</em>
420
456
 
421
457
  </div>