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/logger.ts DELETED
@@ -1,127 +0,0 @@
1
- /**
2
- * @file logger.ts
3
- * @description Provides a flexible logger that can write to the console and/or rotating log files.
4
- * @author ElectronSz
5
- */
6
-
7
- import * as fs from "fs/promises";
8
- import {
9
- LogLevel,
10
- type LoggerConfig,
11
- type PoolMetrics,
12
- StabilizeError,
13
- } from "./types";
14
-
15
- /**
16
- * Defines the interface for a logger that can be used within the ORM.
17
- */
18
- export interface Logger {
19
- logQuery(query: string, params: any[], executionTime?: number): void;
20
- logError(error: Error): void;
21
- logMetrics(metrics: PoolMetrics): void;
22
- logInfo(message: string): void;
23
- logWarn(message: string): void;
24
- logDebug(message: string): void;
25
- }
26
-
27
- /**
28
- * A logger implementation that writes to the console and can optionally write to rotating files.
29
- */
30
- export class StabilizeLogger implements Logger {
31
- private readonly level: LogLevel;
32
- private readonly filePath: string | null;
33
- private readonly maxFileSize: number;
34
- private readonly maxFiles: number;
35
-
36
- constructor(config: LoggerConfig = {}) {
37
- this.level = config.level ?? LogLevel.Info;
38
- this.filePath = config.filePath || null;
39
- this.maxFileSize = config.maxFileSize || 1 * 1024 * 1024; // 1MB
40
- this.maxFiles = config.maxFiles || 3;
41
- }
42
-
43
- /** @internal Checks if a message at a given level should be logged. */
44
- private shouldLog(messageLevel: LogLevel): boolean {
45
- return messageLevel <= this.level;
46
- }
47
-
48
- /** @internal Rotates log files if the current one exceeds the max size. */
49
- private async rotateLogFile(): Promise<void> {
50
- if (!this.filePath) return;
51
-
52
- try {
53
- const stats = await fs.stat(this.filePath).catch(() => null);
54
- if (!stats || stats.size < this.maxFileSize) {
55
- return; // No rotation needed
56
- }
57
-
58
- const oldestLog = `${this.filePath}.${this.maxFiles}`;
59
- await fs.unlink(oldestLog).catch(() => {});
60
-
61
- for (let i = this.maxFiles - 1; i >= 1; i--) {
62
- const source = `${this.filePath}.${i}`;
63
- const destination = `${this.filePath}.${i + 1}`;
64
- if (await fs.stat(source).catch(() => null)) {
65
- await fs.rename(source, destination);
66
- }
67
- }
68
- await fs.rename(this.filePath, `${this.filePath}.1`);
69
- } catch (error) {
70
- // Use StabilizeError for internal logger failures
71
- const logError = new StabilizeError("Log rotation failed", "LOG_ROTATION_ERROR", error as Error);
72
- console.error(`[LOGGER_ERROR] ${logError.message}\n${logError.stack}`);
73
- }
74
- }
75
-
76
- /** @internal Writes a formatted message to the console and/or a file. */
77
- private async log(level: LogLevel, message: string): Promise<void> {
78
- if (!this.shouldLog(level)) return;
79
-
80
- const levelStr = LogLevel[level].toUpperCase();
81
- const logEntry = `[${levelStr}] ${new Date().toISOString()} - ${message}`;
82
-
83
- switch (level) {
84
- case LogLevel.Error: console.error(logEntry); break;
85
- case LogLevel.Warn: console.warn(logEntry); break;
86
- default: console.log(logEntry); break;
87
- }
88
-
89
- if (this.filePath) {
90
- try {
91
- await this.rotateLogFile();
92
- await fs.appendFile(this.filePath, logEntry + "\n");
93
- } catch (error) {
94
- // Use StabilizeError for internal logger failures
95
- const logError = new StabilizeError("Failed to write to log file", "LOG_WRITE_ERROR", error as Error);
96
- console.error(`[LOGGER_ERROR] ${logError.message}\n${logError.stack}`);
97
- }
98
- }
99
- }
100
-
101
- public logQuery(query: string, params: any[], executionTime?: number): void {
102
- const time = executionTime ? `${executionTime.toFixed(2)}ms` : "N/A";
103
- this.log(LogLevel.Debug, `Query: ${query} | Params: ${JSON.stringify(params)} | Time: ${time}`);
104
- }
105
-
106
- public logError(error: Error): void {
107
- const message = `${error.message}${error.stack ? `\n${error.stack}` : ""}`;
108
- this.log(LogLevel.Error, message);
109
- }
110
-
111
- public logMetrics(metrics: PoolMetrics): void {
112
- const message = `Pool Metrics: Active=${metrics.activeConnections}, Idle=${metrics.idleConnections}, Total=${metrics.totalConnections}`;
113
- this.log(LogLevel.Info, message);
114
- }
115
-
116
- public logInfo(message: string): void {
117
- this.log(LogLevel.Info, message);
118
- }
119
-
120
- public logWarn(message: string): void {
121
- this.log(LogLevel.Warn, message);
122
- }
123
-
124
- public logDebug(message: string): void {
125
- this.log(LogLevel.Debug, message);
126
- }
127
- }
package/migrations.ts DELETED
@@ -1,318 +0,0 @@
1
- /**
2
- * @file migrations.ts
3
- * @description Contains functions for generating and running database migrations based on model metadata.
4
- * @author ElectronSz
5
- * @date 2025-10-15 20:55:49
6
- */
7
-
8
- import { DBClient } from "./client";
9
- import { MetadataStorage } from "./model";
10
- import { type DBConfig, type Migration, StabilizeError, DBType, DataTypes } from "./types";
11
-
12
- /**
13
- * @internal
14
- * Formats a SQL query with placeholders for the target database dialect.
15
- * @param query The SQL query string with '?' placeholders.
16
- * @param dbType The target database dialect.
17
- * @returns The formatted SQL query string.
18
- */
19
- function formatQuery(query: string, dbType: DBType): string {
20
- if (dbType === DBType.Postgres) {
21
- let paramIndex = 1;
22
- return query.replace(/\?/g, () => `$${paramIndex++}`);
23
- }
24
- return query;
25
- }
26
-
27
- /**
28
- * Maps an abstract data type to the correct SQL type string for the specified database dialect.
29
- * @param dt The data type to map.
30
- * @param dbType The target database dialect.
31
- * @returns The SQL column type string.
32
- */
33
- function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
34
- let type: string;
35
- if (typeof dt === "string") {
36
- type = dt.toLowerCase();
37
- } else {
38
- type = DataTypes[dt].toLowerCase();
39
- }
40
-
41
- if (dbType === DBType.Postgres) {
42
- switch (type) {
43
- case "string": return "TEXT";
44
- case "text": return "TEXT";
45
- case "integer": return "INTEGER";
46
- case "bigint": return "BIGINT";
47
- case "float": return "REAL";
48
- case "double": return "DOUBLE PRECISION";
49
- case "decimal": return "DECIMAL";
50
- case "boolean": return "BOOLEAN";
51
- case "date": return "DATE";
52
- case "datetime": return "TIMESTAMP";
53
- case "json": return "JSONB";
54
- case "uuid": return "UUID";
55
- case "blob": return "BYTEA";
56
- default: return "TEXT";
57
- }
58
- }
59
- if (dbType === DBType.MySQL) {
60
- switch (type) {
61
- case "string": return "VARCHAR(255)";
62
- case "text": return "TEXT";
63
- case "integer": return "INT";
64
- case "bigint": return "BIGINT";
65
- case "float": return "FLOAT";
66
- case "double": return "DOUBLE";
67
- case "decimal": return "DECIMAL(10,2)";
68
- case "boolean": return "TINYINT(1)";
69
- case "date": return "DATE";
70
- case "datetime": return "DATETIME";
71
- case "json": return "JSON";
72
- case "uuid": return "CHAR(36)";
73
- case "blob": return "BLOB";
74
- default: return "TEXT";
75
- }
76
- }
77
- if (dbType === DBType.SQLite) {
78
- switch (type) {
79
- case "string": return "TEXT";
80
- case "text": return "TEXT";
81
- case "integer": return "INTEGER";
82
- case "bigint": return "INTEGER";
83
- case "float": return "REAL";
84
- case "double": return "REAL";
85
- case "decimal": return "NUMERIC";
86
- case "boolean": return "INTEGER";
87
- case "date": return "TEXT";
88
- case "datetime": return "TEXT";
89
- case "json": return "TEXT";
90
- case "uuid": return "TEXT";
91
- case "blob": return "BLOB";
92
- default: return "TEXT";
93
- }
94
- }
95
- return "TEXT";
96
- }
97
-
98
- /**
99
- * @internal
100
- * Gets the database-specific SQL for an auto-incrementing primary key.
101
- * @param dbType The target database dialect.
102
- * @returns The SQL string for the primary key column definition.
103
- */
104
- function getAutoIncrementPK(dbType: DBType): string {
105
- switch (dbType) {
106
- case DBType.Postgres:
107
- return "SERIAL PRIMARY KEY";
108
- case DBType.MySQL:
109
- return "INT AUTO_INCREMENT PRIMARY KEY";
110
- case DBType.SQLite:
111
- default:
112
- return "INTEGER PRIMARY KEY AUTOINCREMENT";
113
- }
114
- }
115
-
116
- /**
117
- * Generates SQL migration scripts (`up` and `down`) based on a model's configuration.
118
- * @param model The model class defined with `defineModel`.
119
- * @param name A descriptive name for the migration.
120
- * @param dbType The target database dialect.
121
- * @returns A promise that resolves to a `Migration` object containing the `up` and `down` SQL scripts.
122
- */
123
- export async function generateMigration(
124
- model: new (...args: any[]) => any,
125
- name: string,
126
- dbType: DBType,
127
- ): Promise<Migration> {
128
- const tableName = MetadataStorage.getTableName(model);
129
- if (!tableName) {
130
- throw new StabilizeError("Model not defined with tableName", "MIGRATION_ERROR");
131
- }
132
-
133
- const columns = MetadataStorage.getColumns(model);
134
- const validators = MetadataStorage.getValidators(model);
135
- const versioned = MetadataStorage.isVersioned(model);
136
- const timestamps = MetadataStorage.getTimestamps(model);
137
-
138
- const columnDefs: string[] = [];
139
-
140
- for (const [key, col] of Object.entries(columns)) {
141
- const defParts: string[] = [];
142
-
143
- if (col.name === "id") {
144
- defParts.push("id");
145
- defParts.push(getAutoIncrementPK(dbType));
146
- } else {
147
- defParts.push(col.name || key);
148
- defParts.push(mapDataTypeToSql(col.type, dbType));
149
- }
150
-
151
- if (validators[key]?.includes("required")) {
152
- defParts.push("NOT NULL");
153
- }
154
- if (validators[key]?.includes("unique")) {
155
- defParts.push("UNIQUE");
156
- }
157
- if (col.defaultValue !== undefined) {
158
- defParts.push(`DEFAULT ${JSON.stringify(col.defaultValue)}`);
159
- }
160
- if (col.index) {
161
- defParts.push(`INDEX ${col.index}`);
162
- }
163
-
164
- columnDefs.push(defParts.join(" "));
165
- }
166
-
167
- // Add timestamp columns if enabled
168
- if (timestamps) {
169
- for (const [field, colName] of Object.entries(timestamps)) {
170
- // Use the field name defined in the timestamps config
171
- let sqlType = dbType === DBType.Postgres ? "TIMESTAMP" : "DATETIME";
172
- let def = `${colName} ${sqlType} NOT NULL`;
173
-
174
- // Set default value for createdAt, and optionally for updatedAt
175
- if (field === "createdAt") {
176
- def += " DEFAULT CURRENT_TIMESTAMP";
177
- } else if (field === "updatedAt") {
178
- def += " DEFAULT CURRENT_TIMESTAMP";
179
- // For MySQL, add ON UPDATE CURRENT_TIMESTAMP
180
- if (dbType === DBType.MySQL) {
181
- def += " ON UPDATE CURRENT_TIMESTAMP";
182
- }
183
- }
184
-
185
- columnDefs.push(def);
186
- }
187
- }
188
-
189
- const up: string[] = [`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`];
190
- const down: string[] = [`DROP TABLE IF EXISTS ${tableName}`];
191
-
192
- if (versioned) {
193
- const [historyUp, historyDown] = generateHistoryMigration(tableName, columnDefs, dbType);
194
- up.push(historyUp);
195
- down.push(historyDown);
196
- }
197
-
198
- return { up, down, name };
199
- }
200
-
201
- /**
202
- * Generates SQL for a version/audit history table for time-travel queries.
203
- * @param tableName The name of the main table.
204
- * @param columnDefs The column definitions (from the main table).
205
- * @param dbType The target database dialect.
206
- */
207
- function generateHistoryMigration(
208
- tableName: string,
209
- columnDefs: string[],
210
- dbType: DBType,
211
- ): [string, string] {
212
- const historyTable = `${tableName}_history`;
213
- let opType = "VARCHAR(10) NOT NULL";
214
- let versionType = "INT NOT NULL";
215
- let tsType = dbType === DBType.MySQL ? "DATETIME" :
216
- dbType === DBType.SQLite ? "TEXT" : "TIMESTAMP";
217
- let modByType = dbType === DBType.MySQL ? "VARCHAR(255)" : "TEXT";
218
- let modAtType = tsType + (dbType === DBType.Postgres ? " DEFAULT CURRENT_TIMESTAMP" : "");
219
-
220
- // Strip constraints for history columns
221
- function cleanColumnDef(def: string): string {
222
- return def
223
- .replace(/\s+PRIMARY\s+KEY\b/gi, "")
224
- .replace(/\s+UNIQUE\b/gi, "");
225
- }
226
-
227
- const historyColumns = [
228
- ...columnDefs.map(cleanColumnDef),
229
- `operation ${opType}`,
230
- `version ${versionType}`,
231
- `valid_from ${tsType} NOT NULL`,
232
- `valid_to ${tsType}`,
233
- `modified_by ${modByType}`,
234
- `modified_at ${modAtType}`
235
- ];
236
- return [
237
- `CREATE TABLE IF NOT EXISTS ${historyTable} (${historyColumns.join(", ")})`,
238
- `DROP TABLE IF EXISTS ${historyTable}`
239
- ];
240
- }
241
-
242
- /**
243
- * @internal
244
- * Gets the database-specific SQL for creating the `migrations` table.
245
- * @param dbType The target database dialect.
246
- * @returns The SQL string for the `CREATE TABLE` statement.
247
- */
248
- function getMigrationsTableSQL(dbType: DBType): string {
249
- switch (dbType) {
250
- case DBType.Postgres:
251
- return `CREATE TABLE IF NOT EXISTS stabilize_migrations (
252
- id SERIAL PRIMARY KEY,
253
- name VARCHAR(255) UNIQUE NOT NULL,
254
- applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
255
- )`;
256
- case DBType.MySQL:
257
- return `CREATE TABLE IF NOT EXISTS stabilize_migrations (
258
- id INT AUTO_INCREMENT PRIMARY KEY,
259
- name VARCHAR(255) UNIQUE NOT NULL,
260
- applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
261
- )`;
262
- case DBType.SQLite:
263
- default:
264
- return `CREATE TABLE IF NOT EXISTS stabilize_migrations (
265
- id INTEGER PRIMARY KEY AUTOINCREMENT,
266
- name TEXT UNIQUE NOT NULL,
267
- applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
268
- )`;
269
- }
270
- }
271
-
272
- /**
273
- * Connects to the database and runs all pending migrations.
274
- * @param config The database configuration object.
275
- * @param migrations An array of `Migration` objects to be executed.
276
- */
277
- export async function runMigrations(config: DBConfig, migrations: Migration[]) {
278
- const client = new DBClient(config);
279
- try {
280
- const dbType = config.type;
281
- await client.query(getMigrationsTableSQL(dbType));
282
-
283
- for (const [index, migration] of migrations.entries()) {
284
- const name = migration.name || `migration_${index}_${new Date().getTime()}`;
285
-
286
- const selectQuery = formatQuery(`SELECT id FROM stabilize_migrations WHERE name = ?`, dbType);
287
- const applied = await client.query<{ id: number }>(selectQuery, [name]);
288
-
289
- if (applied.length === 0) {
290
- await client.transaction(async (txClient) => {
291
- console.log(`Applying migration: ${name}...`);
292
- for (const query of migration.up) {
293
- await txClient.query(query);
294
- }
295
-
296
- const insertQuery = formatQuery(`INSERT INTO stabilize_migrations (name, applied_at) VALUES (?, ?)`, dbType);
297
- let appliedAt: string;
298
- if (dbType === DBType.MySQL) {
299
- appliedAt = new Date().toISOString().slice(0, 19).replace("T", " ");
300
- } else {
301
- appliedAt = new Date().toISOString();
302
- }
303
- await txClient.query(insertQuery, [name, appliedAt]);
304
-
305
- console.log(`Migration ${name} applied successfully.`);
306
- });
307
- }
308
- }
309
- } catch (error) {
310
- console.error("Migration failed:", error);
311
- throw error;
312
- } finally {
313
- await client.close();
314
- }
315
- }
316
-
317
- export type { Migration };
318
- export { mapDataTypeToSql };
package/query-builder.ts DELETED
@@ -1,209 +0,0 @@
1
- /**
2
- * @file query-builder.ts
3
- * @description Provides a fluent API for building and executing SQL queries in a database-agnostic way.
4
- * @author ElectronSz
5
- */
6
-
7
- import { DBClient } from "./client";
8
- import { Cache } from "./cache";
9
- import { MetadataStorage } from "./model";
10
- import { StabilizeError } from "./types";
11
-
12
- /**
13
- * A fluent interface for building SQL SELECT queries.
14
- * This class allows for the programmatic and readable construction of queries
15
- * that can be executed on different database systems via the DBClient.
16
- * @template T The type of the entity being queried.
17
- */
18
- export class QueryBuilder<T> {
19
- private table: string;
20
- private selectFields: string[] = ["*"];
21
- private joins: string[] = [];
22
- private whereConditions: string[] = [];
23
- private whereParams: any[] = [];
24
- private orderByClause: string | null = null;
25
- private limitValue: number | null = null;
26
- private offsetValue: number | null = null;
27
-
28
- /**
29
- * Creates an instance of QueryBuilder.
30
- * @param table The name of the main table to query from.
31
- */
32
- constructor(table: string) {
33
- this.table = table;
34
- }
35
-
36
- /**
37
- * Specifies the columns to select. If not called, all columns (`*`) are selected by default.
38
- * @param fields A list of column names to select.
39
- * @returns The `QueryBuilder` instance for chaining.
40
- * @example
41
- * ```
42
- * queryBuilder.select('id', 'name', 'email');
43
- * ```
44
- */
45
- select(...fields: string[]): QueryBuilder<T> {
46
- this.selectFields = fields.length > 0 ? fields : ["*"];
47
- return this;
48
- }
49
-
50
- /**
51
- * Adds a WHERE clause to the query. Multiple calls will be joined with AND.
52
- * @param condition The SQL condition string with `?` as placeholders.
53
- * @param params The values to substitute for the `?` placeholders.
54
- * @returns The `QueryBuilder` instance for chaining.
55
- * @example
56
- * ```
57
- * queryBuilder.where('status = ?', 'active').where('age > ?', 21);
58
- * ```
59
- */
60
- where(condition: string, ...params: any[]): QueryBuilder<T> {
61
- this.whereConditions.push(condition);
62
- this.whereParams.push(...params);
63
- return this;
64
- }
65
-
66
- /**
67
- * Adds a LEFT JOIN clause to the query.
68
- * @param table The name of the table to join with.
69
- * @param condition The ON condition for the join.
70
- * @returns The `QueryBuilder` instance for chaining.
71
- * @example
72
- * ```
73
- * queryBuilder.join('profiles', 'profiles.userId = users.id');
74
- * ```
75
- */
76
- join(table: string, condition: string): QueryBuilder<T> {
77
- this.joins.push(`LEFT JOIN ${table} ON ${condition}`);
78
- return this;
79
- }
80
-
81
- /**
82
- * Adds an ORDER BY clause to the query.
83
- * @param clause The column and direction for ordering (e.g., 'createdAt DESC').
84
- * @returns The `QueryBuilder` instance for chaining.
85
- * @example
86
- * ```
87
- * queryBuilder.orderBy('lastName ASC');
88
- * ```
89
- */
90
- orderBy(clause: string): QueryBuilder<T> {
91
- this.orderByClause = clause;
92
- return this;
93
- }
94
-
95
- /**
96
- * Adds a LIMIT clause to the query to restrict the number of rows returned.
97
- * @param limit The maximum number of rows to return.
98
- * @returns The `QueryBuilder` instance for chaining.
99
- * @example
100
- * ```
101
- * queryBuilder.limit(10);
102
- * ```
103
- */
104
- limit(limit: number): QueryBuilder<T> {
105
- this.limitValue = limit;
106
- return this;
107
- }
108
-
109
- /**
110
- * Adds an OFFSET clause to the query for pagination.
111
- * @param offset The number of rows to skip.
112
- * @returns The `QueryBuilder` instance for chaining.
113
- * @example
114
- * ```
115
- * queryBuilder.offset(20);
116
- * ```
117
- */
118
- offset(offset: number): QueryBuilder<T> {
119
- this.offsetValue = offset;
120
- return this;
121
- }
122
-
123
- /**
124
- * Constructs the final SQL query string and its corresponding parameters.
125
- * This is an internal method, typically called by `execute`.
126
- * @returns An object containing the final `query` string and `params` array.
127
- */
128
- build(): { query: string; params: any[] } {
129
- let query = `SELECT ${this.selectFields.join(", ")} FROM ${this.table}`;
130
-
131
- if (this.joins.length > 0) {
132
- query += " " + this.joins.join(" ");
133
- }
134
- if (this.whereConditions.length > 0) {
135
- query += " WHERE " + this.whereConditions.join(" AND ");
136
- }
137
- if (this.orderByClause) {
138
- query += ` ORDER BY ${this.orderByClause}`;
139
- }
140
- if (this.limitValue !== null) {
141
- query += ` LIMIT ${this.limitValue}`;
142
- }
143
- if (this.offsetValue !== null) {
144
- query += ` OFFSET ${this.offsetValue}`;
145
- }
146
- return { query, params: this.whereParams };
147
- }
148
-
149
- /**
150
- * Executes the constructed query against the database using the provided client.
151
- * Handles cache-aside logic if a cache and cacheKey are provided.
152
- * @param client The `DBClient` instance to use for executing the query.
153
- * @param cache Optional: The `Cache` instance to use for caching.
154
- * @param cacheKey Optional: The key to use for getting/setting the result in the cache.
155
- * @returns A promise that resolves to an array of results of type `T`.
156
- * @example
157
- * ```
158
- * const users = await stabilize.getRepository(User)
159
- * .find()
160
- * .where('status = ?', 'active')
161
- * .limit(10)
162
- * .execute(dbClient, cache, 'active_users_page_1');
163
- * ```
164
- */
165
- async execute(
166
- client: DBClient,
167
- cache?: Cache,
168
- cacheKey?: string,
169
- ): Promise<T[]> {
170
- const { query, params } = this.build();
171
-
172
- // Attempt to retrieve from cache first (cache-aside read)
173
- if (cache && cacheKey) {
174
- const cached = await cache.get<T[]>(cacheKey);
175
- if (cached) return cached;
176
- }
177
-
178
- // If not in cache, execute query against the database
179
- const results = await client.query<T>(query, params);
180
-
181
- // Store the database results in the cache for future requests
182
- if (cache && cacheKey && results.length > 0) {
183
- await cache.set(cacheKey, results, 60);
184
- }
185
-
186
- return results;
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
-
209
- }