stabilize-orm 1.3.0 → 1.3.2

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/README.md CHANGED
@@ -25,6 +25,7 @@ _A Modern, Type-Safe, and Expressive ORM for Bun_
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.
28
+ - **Custom Query Scopes**: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
28
29
 
29
30
  ---
30
31
 
@@ -358,11 +359,55 @@ console.log(activeAdmins);
358
359
  orderBy(clause: string): QueryBuilder<User>;
359
360
  limit(limit: number): QueryBuilder<User>;
360
361
  offset(offset: number): QueryBuilder<User>;
362
+ scope(name: string, ...args: any[]): QueryBuilder<User>;
361
363
  build(): { query: string; params: any[] };
362
364
  execute(client?: DBClient, cache?: Cache, cacheKey?: string): Promise<User[]>;
363
365
  }
364
366
  ```
365
367
 
368
+ ### Custom Query Scopes
369
+
370
+ 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.
371
+
372
+ #### **Scopes Example**
373
+
374
+ ```typescript
375
+ import { defineModel, DataTypes } from "stabilize-orm";
376
+ import { orm } from "./db";
377
+
378
+ const User = defineModel({
379
+ tableName: "users",
380
+ columns: {
381
+ id: { type: DataTypes.Integer, required: true },
382
+ email: { type: DataTypes.String, length: 100, required: true },
383
+ isActive: { type: DataTypes.Boolean, required: true },
384
+ createdAt: { type: DataTypes.DateTime },
385
+ },
386
+ scopes: {
387
+ active: (qb) => qb.where("isActive = ?", true),
388
+ recent: (qb, days: number) => qb.where("createdAt >= ?", new Date(Date.now() - days * 24 * 60 * 60 * 1000)),
389
+ },
390
+ });
391
+
392
+ const userRepository = orm.getRepository(User);
393
+
394
+ // Fetch active users
395
+ const activeUsers = await userRepository.scope("active").execute();
396
+
397
+ // Fetch users created in the last 7 days
398
+ const recentUsers = await userRepository.scope("recent", 7).execute();
399
+
400
+ // Combine scopes with other query operations
401
+ const recentActiveUsers = await userRepository
402
+ .scope("active")
403
+ .scope("recent", 7)
404
+ .orderBy("createdAt DESC")
405
+ .limit(10)
406
+ .execute();
407
+
408
+ console.log(recentActiveUsers);
409
+ ```
410
+
366
411
  ---
367
412
 
368
413
  ## 🗑️ Soft Deletes
@@ -452,6 +497,6 @@ Licensed under the MIT License. See [LICENSE.md](./LICENSE.md) for details.
452
497
 
453
498
  Created with ❤️ by **ElectronSz**
454
499
  <br/>
455
- <em>File last updated: 2025-10-18 22:10:00 SAST</em>
500
+ <em>File last updated: 2025-10-19 10:24:00 SAST</em>
456
501
 
457
502
  </div>
package/model.ts CHANGED
@@ -1,10 +1,10 @@
1
-
2
1
  /**
3
2
  * @file model.ts
4
3
  * @description Provides a programmatic API for defining models and a metadata storage system.
5
4
  * @author ElectronSz
6
5
  */
7
6
 
7
+ import type { QueryBuilder } from './query-builder';
8
8
  import { DataTypes, RelationType, DBType } from './types';
9
9
 
10
10
  // Interface for column configuration
@@ -38,28 +38,57 @@ export interface ModelConfig {
38
38
  softDelete?: boolean;
39
39
  columns: Record<string, ColumnConfig>;
40
40
  relations?: RelationConfig[];
41
+ scopes?: Record<string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>>; // Custom query scopes
41
42
  }
42
43
 
43
- // Metadata storage for models
44
+ /**
45
+ * Metadata storage for models.
46
+ * Stores and retrieves model configuration such as columns, relations, scopes, etc.
47
+ */
44
48
  export class MetadataStorage {
45
49
  private static models: Map<Function, ModelConfig> = new Map();
46
50
 
51
+ /**
52
+ * Associates model metadata with a class constructor.
53
+ * @param model - The class constructor for the model.
54
+ * @param config - The model configuration object.
55
+ */
47
56
  static setModelMetadata(model: Function, config: ModelConfig) {
48
57
  this.models.set(model, config);
49
58
  }
50
59
 
60
+ /**
61
+ * Retrieves the model configuration for a given model class.
62
+ * @param model - The class constructor for the model.
63
+ * @returns The model configuration or undefined if not found.
64
+ */
51
65
  static getModelMetadata(model: Function): ModelConfig | undefined {
52
66
  return this.models.get(model);
53
67
  }
54
68
 
69
+ /**
70
+ * Gets the table name for a given model class.
71
+ * @param model - The class constructor for the model.
72
+ * @returns The table name or an empty string if not found.
73
+ */
55
74
  static getTableName(model: Function): string {
56
75
  return this.getModelMetadata(model)?.tableName || '';
57
76
  }
58
77
 
78
+ /**
79
+ * Gets the column configuration for a given model class.
80
+ * @param model - The class constructor for the model.
81
+ * @returns Record of column names to their configuration.
82
+ */
59
83
  static getColumns(model: Function): Record<string, ColumnConfig> {
60
84
  return this.getModelMetadata(model)?.columns || {};
61
85
  }
62
86
 
87
+ /**
88
+ * Collects validation rules for each column of a given model.
89
+ * @param model - The class constructor for the model.
90
+ * @returns An object mapping column names to an array of validation rule names.
91
+ */
63
92
  static getValidators(model: Function): Record<string, string[]> {
64
93
  const columns = this.getModelMetadata(model)?.columns || {};
65
94
  const validators: Record<string, string[]> = {};
@@ -72,6 +101,11 @@ export class MetadataStorage {
72
101
  return validators;
73
102
  }
74
103
 
104
+ /**
105
+ * Gets the relationship configuration for a given model class.
106
+ * @param model - The class constructor for the model.
107
+ * @returns Record of property names to their relation configuration.
108
+ */
75
109
  static getRelations(model: Function): Record<string, RelationConfig> {
76
110
  const relations = this.getModelMetadata(model)?.relations || [];
77
111
  const result: Record<string, RelationConfig> = {};
@@ -81,6 +115,11 @@ export class MetadataStorage {
81
115
  return result;
82
116
  }
83
117
 
118
+ /**
119
+ * Finds the soft delete field, if any, for a given model class.
120
+ * @param model - The class constructor for the model.
121
+ * @returns The key of the soft delete field, or null if not found.
122
+ */
84
123
  static getSoftDeleteField(model: Function): string | null {
85
124
  const columns = this.getModelMetadata(model)?.columns || {};
86
125
  for (const [key, col] of Object.entries(columns)) {
@@ -89,14 +128,36 @@ export class MetadataStorage {
89
128
  return null;
90
129
  }
91
130
 
131
+ /**
132
+ * Checks if the model is versioned.
133
+ * @param model - The class constructor for the model.
134
+ * @returns True if versioned, false otherwise.
135
+ */
92
136
  static isVersioned(model: Function): boolean {
93
137
  return !!this.getModelMetadata(model)?.versioned;
94
138
  }
139
+
140
+ /**
141
+ * Gets custom query scopes for a given model class.
142
+ * @param model - The class constructor for the model.
143
+ * @returns Record of scope names to scope functions.
144
+ */
145
+ static getScopes(model: Function): Record<string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>> {
146
+ return this.getModelMetadata(model)?.scopes || {};
147
+ }
95
148
  }
96
149
 
97
- // Programmatic model definition
150
+ /**
151
+ * Programmatically defines a model and stores its metadata.
152
+ * @param config - The model configuration object.
153
+ * @returns The dynamically created model class.
154
+ */
98
155
  export function defineModel(config: ModelConfig) {
99
156
  class Model {
157
+ /**
158
+ * Constructs a model instance from plain data.
159
+ * @param data - The plain object to assign properties from.
160
+ */
100
161
  constructor(data: any) {
101
162
  Object.assign(this, data);
102
163
  }
@@ -109,7 +170,8 @@ export function defineModel(config: ModelConfig) {
109
170
  softDelete: config.softDelete || false,
110
171
  columns: config.columns,
111
172
  relations: config.relations || [],
173
+ scopes: config.scopes || {},
112
174
  });
113
175
 
114
176
  return Model;
115
- }
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stabilize-orm",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/query-builder.ts CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { DBClient } from "./client";
8
8
  import { Cache } from "./cache";
9
+ import { MetadataStorage } from "./model";
10
+ import { StabilizeError } from "./types";
9
11
 
10
12
  /**
11
13
  * A fluent interface for building SQL SELECT queries.
@@ -125,7 +127,7 @@ export class QueryBuilder<T> {
125
127
  */
126
128
  build(): { query: string; params: any[] } {
127
129
  let query = `SELECT ${this.selectFields.join(", ")} FROM ${this.table}`;
128
-
130
+
129
131
  if (this.joins.length > 0) {
130
132
  query += " " + this.joins.join(" ");
131
133
  }
@@ -166,7 +168,7 @@ export class QueryBuilder<T> {
166
168
  cacheKey?: string,
167
169
  ): Promise<T[]> {
168
170
  const { query, params } = this.build();
169
-
171
+
170
172
  // Attempt to retrieve from cache first (cache-aside read)
171
173
  if (cache && cacheKey) {
172
174
  const cached = await cache.get<T[]>(cacheKey);
@@ -178,9 +180,30 @@ export class QueryBuilder<T> {
178
180
 
179
181
  // Store the database results in the cache for future requests
180
182
  if (cache && cacheKey && results.length > 0) {
181
- await cache.set(cacheKey, results, 60);
183
+ await cache.set(cacheKey, results, 60);
182
184
  }
183
-
185
+
184
186
  return results;
185
187
  }
188
+
189
+ /**
190
+ * Applies a named scope to the current query builder.
191
+ *
192
+ * This method looks up a scope function by name for the current model (based on the table name),
193
+ * then invokes the scope function with the query builder and any additional arguments.
194
+ *
195
+ * @param {string} name - The name of the scope to apply.
196
+ * @param {...any} args - Additional arguments to pass to the scope function.
197
+ * @throws {StabilizeError} If no model is found for the current table, or if the specified scope does not exist.
198
+ * @returns {QueryBuilder<T>} The query builder instance after applying the scope.
199
+ */
200
+ scope(name: string, ...args: any[]): QueryBuilder<T> {
201
+ const model = Object.values(MetadataStorage['models']).find(m => m.tableName === this.table)?.constructor;
202
+ if (!model) throw new StabilizeError(`Model for table ${this.table} not found`, "SCOPE_ERROR");
203
+ const scopes = MetadataStorage.getScopes(model);
204
+ const scopeFn = scopes[name];
205
+ if (!scopeFn) throw new StabilizeError(`Scope ${name} not found`, "SCOPE_ERROR");
206
+ return scopeFn(this, ...args);
207
+ }
208
+
186
209
  }
package/repository.ts CHANGED
@@ -148,6 +148,21 @@ export class Repository<T> {
148
148
  return qb;
149
149
  }
150
150
 
151
+ /**
152
+ * Applies a custom scope to the query for the repository's table.
153
+ * @param name The name of the scope to apply.
154
+ * @param args Optional arguments to pass to the scope function.
155
+ * @returns A `QueryBuilder` instance with the scope applied.
156
+ * @example
157
+ * ```
158
+ * const activeUsers = await userRepository.scope('active').execute(client);
159
+ * ```
160
+ */
161
+ scope(name: string, ...args: any[]): QueryBuilder<T> {
162
+ this.logger.logDebug(`Applying scope ${name} to ${this.table}`);
163
+ return this.find().scope(name, ...args);
164
+ }
165
+
151
166
  /**
152
167
  * Finds a single record by its primary key (id).
153
168
  * @param id The ID of the record to find.
package/types.ts CHANGED
@@ -42,6 +42,7 @@ export enum DataTypes {
42
42
  JSON, // Maps to JSON, JSONB, or TEXT
43
43
  UUID, // Maps to UUID or VARCHAR(36)
44
44
  BLOB, // Maps to BYTEA or BLOB
45
+
45
46
  }
46
47
 
47
48
  export interface DBConfig {