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 +46 -1
- package/model.ts +66 -4
- package/package.json +1 -1
- package/query-builder.ts +27 -4
- package/repository.ts +15 -0
- package/types.ts +1 -0
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
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.
|