stabilize-orm 1.1.3 → 1.1.4
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 +209 -155
- package/bun.lock +61 -0
- package/cache.ts +88 -17
- package/cli/stabilize-cli.ts +300 -432
- package/client.ts +153 -170
- package/decorators.ts +70 -4
- package/dist/cli/stabilize-cli.js +3093 -86
- package/dist/index.js +3023 -26
- package/index.ts +90 -51
- package/logger.ts +76 -75
- package/migrations.ts +157 -65
- package/package.json +5 -2
- package/query-builder.ts +103 -13
- package/repository.ts +447 -287
- package/types.ts +58 -29
package/migrations.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file migrations.ts
|
|
3
|
+
* @description Contains functions for generating and running database migrations based on model metadata.
|
|
4
|
+
* @author ElectronSz
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import { DBClient } from "./client";
|
|
2
8
|
import { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey } from "./decorators";
|
|
3
9
|
import { type DBConfig, type Migration, StabilizeError, DBType } from "./types";
|
|
@@ -6,138 +12,224 @@ type ColumnData = { name: string; type: string };
|
|
|
6
12
|
type ColumnMetadata = Record<string, ColumnData>;
|
|
7
13
|
type ValidatorMetadata = Record<string, string[]>;
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
* Gets the database-specific SQL for an auto-incrementing primary key.
|
|
18
|
+
* @param dbType The target database dialect.
|
|
19
|
+
* @returns The SQL string for the primary key column definition.
|
|
20
|
+
*/
|
|
21
|
+
function getAutoIncrementPK(dbType: DBType): string {
|
|
11
22
|
switch (dbType) {
|
|
12
23
|
case DBType.Postgres:
|
|
13
24
|
return "SERIAL PRIMARY KEY";
|
|
14
25
|
case DBType.MySQL:
|
|
15
26
|
return "INT AUTO_INCREMENT PRIMARY KEY";
|
|
16
27
|
case DBType.SQLite:
|
|
28
|
+
default:
|
|
17
29
|
return "INTEGER PRIMARY KEY AUTOINCREMENT";
|
|
18
|
-
default: // Default to Postgres
|
|
19
|
-
return "SERIAL PRIMARY KEY";
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
* Gets the database-specific SQL for a timestamp column.
|
|
36
|
+
* @param dbType The target database dialect.
|
|
37
|
+
* @returns The SQL string for the timestamp column type.
|
|
38
|
+
*/
|
|
39
|
+
function getTimestampType(dbType: DBType): string {
|
|
25
40
|
switch (dbType) {
|
|
26
41
|
case DBType.Postgres:
|
|
27
42
|
return "TIMESTAMP";
|
|
28
43
|
case DBType.MySQL:
|
|
29
44
|
return "DATETIME";
|
|
30
45
|
case DBType.SQLite:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
default:
|
|
47
|
+
return "TEXT";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @internal
|
|
53
|
+
* Gets the database-specific SQL for a default `CURRENT_TIMESTAMP` value.
|
|
54
|
+
* @param dbType The target database dialect.
|
|
55
|
+
* @returns The SQL string for the default value.
|
|
56
|
+
*/
|
|
57
|
+
function getTimestampDefault(dbType: DBType): string {
|
|
58
|
+
switch (dbType) {
|
|
59
|
+
case DBType.Postgres:
|
|
60
|
+
case DBType.SQLite:
|
|
61
|
+
return "DEFAULT CURRENT_TIMESTAMP";
|
|
62
|
+
case DBType.MySQL:
|
|
63
|
+
return "DEFAULT CURRENT_TIMESTAMP";
|
|
64
|
+
default:
|
|
65
|
+
return "DEFAULT CURRENT_TIMESTAMP";
|
|
34
66
|
}
|
|
35
67
|
}
|
|
36
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Generates SQL migration scripts (`up` and `down`) based on a model's decorators.
|
|
71
|
+
* This function reads the metadata from a model class to create a `CREATE TABLE` statement.
|
|
72
|
+
*
|
|
73
|
+
* @param model The model class decorated with `@Model` and `@Column`.
|
|
74
|
+
* @param name A descriptive name for the migration (used for the migration object).
|
|
75
|
+
* @param dbType The target database dialect to generate SQL for. Defaults to Postgres.
|
|
76
|
+
* @returns A promise that resolves to a `Migration` object containing the `up` and `down` SQL scripts.
|
|
77
|
+
* @example
|
|
78
|
+
* ```
|
|
79
|
+
* // In a script like 'scripts/generate_user_migration.ts'
|
|
80
|
+
* import { generateMigration, DBType } from 'stabilize-orm';
|
|
81
|
+
* import { User } from './models/user';
|
|
82
|
+
* import fs from 'fs';
|
|
83
|
+
*
|
|
84
|
+
* async function createMigration() {
|
|
85
|
+
* const migration = await generateMigration(User, 'create_users_table', DBType.Postgres);
|
|
86
|
+
* fs.writeFileSync(
|
|
87
|
+
* `migrations/${new Date().getTime()}_create_users.json`,
|
|
88
|
+
* JSON.stringify(migration, null, 2)
|
|
89
|
+
* );
|
|
90
|
+
* }
|
|
91
|
+
*
|
|
92
|
+
* createMigration();
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
37
95
|
export async function generateMigration(
|
|
38
96
|
model: new (...args: any[]) => any,
|
|
39
|
-
name: string,
|
|
40
|
-
dbType: DBType = DBType.Postgres,
|
|
97
|
+
name: string,
|
|
98
|
+
dbType: DBType = DBType.Postgres,
|
|
41
99
|
): Promise<Migration> {
|
|
42
100
|
const tableName = Reflect.getMetadata(ModelKey, model);
|
|
43
|
-
if (!tableName)
|
|
44
|
-
throw new StabilizeError(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const columns: ColumnMetadata =
|
|
50
|
-
Reflect.getMetadata(ColumnKey, model.prototype) || {};
|
|
51
|
-
const validators: ValidatorMetadata =
|
|
52
|
-
Reflect.getMetadata(ValidatorKey, model.prototype) || {};
|
|
101
|
+
if (!tableName) {
|
|
102
|
+
throw new StabilizeError("Model not decorated with @Model", "MIGRATION_ERROR");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const columns: ColumnMetadata = Reflect.getMetadata(ColumnKey, model.prototype) || {};
|
|
106
|
+
const validators: ValidatorMetadata = Reflect.getMetadata(ValidatorKey, model.prototype) || {};
|
|
53
107
|
const softDeleteField = Reflect.getMetadata(SoftDeleteKey, model.prototype);
|
|
54
108
|
|
|
55
109
|
const columnDefs = Object.entries(columns).map(([key, col]) => {
|
|
56
|
-
let def: string;
|
|
57
110
|
if (col.name === "id") {
|
|
58
|
-
|
|
59
|
-
def = getAutoIncrementPK(dbType);
|
|
60
|
-
return def;
|
|
111
|
+
return `id ${getAutoIncrementPK(dbType)}`;
|
|
61
112
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
|
|
114
|
+
const defParts: string[] = [col.name];
|
|
115
|
+
|
|
116
|
+
if (["createdAt", "updatedAt"].includes(key) || (softDeleteField && key === softDeleteField)) {
|
|
117
|
+
defParts.push(getTimestampType(dbType));
|
|
118
|
+
} else {
|
|
119
|
+
defParts.push(col.type);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (validators[key]?.includes("required")) {
|
|
123
|
+
defParts.push("NOT NULL");
|
|
124
|
+
}
|
|
125
|
+
if (validators[key]?.includes("unique")) {
|
|
126
|
+
defParts.push("UNIQUE");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (key === "createdAt") {
|
|
130
|
+
defParts.push(getTimestampDefault(dbType));
|
|
68
131
|
}
|
|
69
|
-
|
|
132
|
+
|
|
133
|
+
return defParts.join(" ");
|
|
70
134
|
});
|
|
71
135
|
|
|
72
|
-
if (softDeleteField && columns[softDeleteField]) {
|
|
73
|
-
columnDefs.push(
|
|
74
|
-
`${columns[softDeleteField].name} ${columns[softDeleteField].type}`,
|
|
75
|
-
);
|
|
136
|
+
if (softDeleteField && !columns[softDeleteField]) {
|
|
137
|
+
columnDefs.push(`${softDeleteField} ${getTimestampType(dbType)}`);
|
|
76
138
|
}
|
|
77
139
|
|
|
78
|
-
const up = [
|
|
79
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`,
|
|
80
|
-
];
|
|
140
|
+
const up = [`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`];
|
|
81
141
|
const down = [`DROP TABLE IF EXISTS ${tableName}`];
|
|
82
142
|
|
|
83
|
-
return { up, down };
|
|
143
|
+
return { up, down, name: tableName };
|
|
84
144
|
}
|
|
85
145
|
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
/**
|
|
147
|
+
* @internal
|
|
148
|
+
* Gets the database-specific SQL for creating the `migrations` table, which tracks applied migrations.
|
|
149
|
+
* @param dbType The target database dialect.
|
|
150
|
+
* @returns The SQL string for the `CREATE TABLE` statement.
|
|
151
|
+
*/
|
|
152
|
+
function getMigrationsTableSQL(dbType: DBType): string {
|
|
88
153
|
switch (dbType) {
|
|
89
154
|
case DBType.Postgres:
|
|
90
155
|
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
91
156
|
id SERIAL PRIMARY KEY,
|
|
92
|
-
name
|
|
93
|
-
applied_at TIMESTAMP NOT NULL
|
|
157
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
158
|
+
applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
94
159
|
)`;
|
|
95
160
|
case DBType.MySQL:
|
|
96
161
|
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
97
162
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
98
|
-
name VARCHAR(255) NOT NULL,
|
|
99
|
-
applied_at DATETIME NOT NULL
|
|
163
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
164
|
+
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
100
165
|
)`;
|
|
101
166
|
case DBType.SQLite:
|
|
167
|
+
default:
|
|
102
168
|
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
103
169
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
-
name TEXT NOT NULL,
|
|
105
|
-
applied_at TEXT NOT NULL
|
|
106
|
-
)`;
|
|
107
|
-
default: // Default to Postgres
|
|
108
|
-
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
109
|
-
id SERIAL PRIMARY KEY,
|
|
110
|
-
name TEXT NOT NULL,
|
|
111
|
-
applied_at TIMESTAMP NOT NULL
|
|
170
|
+
name TEXT UNIQUE NOT NULL,
|
|
171
|
+
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
112
172
|
)`;
|
|
113
173
|
}
|
|
114
174
|
}
|
|
115
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Connects to the database and runs all pending migrations.
|
|
178
|
+
* It tracks which migrations have been applied by using a `migrations` table in the database.
|
|
179
|
+
* Each migration is run within a transaction to ensure atomicity.
|
|
180
|
+
*
|
|
181
|
+
* @param config The database configuration object.
|
|
182
|
+
* @param migrations An array of `Migration` objects to be executed.
|
|
183
|
+
* @example
|
|
184
|
+
* ```
|
|
185
|
+
* // In a script like 'scripts/run_all_migrations.ts'
|
|
186
|
+
* import { runMigrations } from 'stabilize-orm';
|
|
187
|
+
* import { dbConfig } from './config';
|
|
188
|
+
* import migration1 from '../migrations/1_create_users.json';
|
|
189
|
+
* import migration2 from '../migrations.ts/2_create_profiles.json';
|
|
190
|
+
*
|
|
191
|
+
* const allMigrations = [migration1, migration2];
|
|
192
|
+
*
|
|
193
|
+
* async function applyMigrations() {
|
|
194
|
+
* console.log('Starting migration process...');
|
|
195
|
+
* await runMigrations(dbConfig, allMigrations);
|
|
196
|
+
* console.log('All pending migrations applied successfully.');
|
|
197
|
+
* }
|
|
198
|
+
*
|
|
199
|
+
* applyMigrations();
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
116
202
|
export async function runMigrations(config: DBConfig, migrations: Migration[]) {
|
|
117
203
|
const client = new DBClient(config);
|
|
118
204
|
try {
|
|
119
|
-
|
|
120
|
-
const dbType = config.type ?? DBType.Postgres;
|
|
121
|
-
|
|
205
|
+
const dbType = config.type;
|
|
122
206
|
await client.query(getMigrationsTableSQL(dbType));
|
|
123
207
|
|
|
124
208
|
for (const [index, migration] of migrations.entries()) {
|
|
125
|
-
const name = `migration_${index}_${new Date().
|
|
209
|
+
const name = migration.name || `migration_${index}_${new Date().getTime()}`;
|
|
210
|
+
|
|
126
211
|
const applied = await client.query<{ id: number }>(
|
|
127
212
|
`SELECT id FROM migrations WHERE name = ?`,
|
|
128
213
|
[name],
|
|
129
214
|
);
|
|
130
215
|
|
|
131
216
|
if (applied.length === 0) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
217
|
+
await client.transaction(async (txClient) => {
|
|
218
|
+
console.log(`Applying migration: ${name}...`);
|
|
219
|
+
for (const query of migration.up) {
|
|
220
|
+
await txClient.query(query);
|
|
221
|
+
}
|
|
222
|
+
await txClient.query(
|
|
223
|
+
`INSERT INTO migrations (name, applied_at) VALUES (?, ?)`,
|
|
224
|
+
[name, new Date().toISOString()],
|
|
225
|
+
);
|
|
226
|
+
console.log(`Migration ${name} applied successfully.`);
|
|
227
|
+
});
|
|
139
228
|
}
|
|
140
229
|
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error("Migration failed:", error);
|
|
232
|
+
throw error;
|
|
141
233
|
} finally {
|
|
142
234
|
await client.close();
|
|
143
235
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stabilize-orm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"stabilize": "
|
|
8
|
+
"stabilize": "dist/cli/stabilize-cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "bun build src/index.ts --outdir dist --target bun --minify && bun build cli/stabilize-cli.ts --outdir dist/cli --target bun --minify",
|
|
@@ -28,11 +28,14 @@
|
|
|
28
28
|
"author": "ElectronSz <lwazicd@icloud.com>",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@types/pg": "^8.15.5",
|
|
31
32
|
"@types/uuid": "^11.0.0",
|
|
32
33
|
"commander": "^12.1.0",
|
|
33
34
|
"figlet": "^1.9.3",
|
|
34
35
|
"glob": "^11.0.0",
|
|
35
36
|
"ioredis": "^5.4.1",
|
|
37
|
+
"mysql2": "^3.15.2",
|
|
38
|
+
"pg": "^8.16.3",
|
|
36
39
|
"reflect-metadata": "^0.2.2",
|
|
37
40
|
"uuid": "^13.0.0"
|
|
38
41
|
},
|
package/query-builder.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
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
|
+
|
|
1
7
|
import { DBClient } from "./client";
|
|
2
8
|
import { Cache } from "./cache";
|
|
3
|
-
import { type QueryHint } from "./types";
|
|
4
9
|
|
|
10
|
+
/**
|
|
11
|
+
* A fluent interface for building SQL SELECT queries.
|
|
12
|
+
* This class allows for the programmatic and readable construction of queries
|
|
13
|
+
* that can be executed on different database systems via the DBClient.
|
|
14
|
+
* @template T The type of the entity being queried.
|
|
15
|
+
*/
|
|
5
16
|
export class QueryBuilder<T> {
|
|
6
17
|
private table: string;
|
|
7
18
|
private selectFields: string[] = ["*"];
|
|
@@ -11,54 +22,110 @@ export class QueryBuilder<T> {
|
|
|
11
22
|
private orderByClause: string | null = null;
|
|
12
23
|
private limitValue: number | null = null;
|
|
13
24
|
private offsetValue: number | null = null;
|
|
14
|
-
private hints: QueryHint[] = [];
|
|
15
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Creates an instance of QueryBuilder.
|
|
28
|
+
* @param table The name of the main table to query from.
|
|
29
|
+
*/
|
|
16
30
|
constructor(table: string) {
|
|
17
31
|
this.table = table;
|
|
18
32
|
}
|
|
19
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Specifies the columns to select. If not called, all columns (`*`) are selected by default.
|
|
36
|
+
* @param fields A list of column names to select.
|
|
37
|
+
* @returns The `QueryBuilder` instance for chaining.
|
|
38
|
+
* @example
|
|
39
|
+
* ```
|
|
40
|
+
* queryBuilder.select('id', 'name', 'email');
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
20
43
|
select(...fields: string[]): QueryBuilder<T> {
|
|
21
44
|
this.selectFields = fields.length > 0 ? fields : ["*"];
|
|
22
45
|
return this;
|
|
23
46
|
}
|
|
24
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Adds a WHERE clause to the query. Multiple calls will be joined with AND.
|
|
50
|
+
* @param condition The SQL condition string with `?` as placeholders.
|
|
51
|
+
* @param params The values to substitute for the `?` placeholders.
|
|
52
|
+
* @returns The `QueryBuilder` instance for chaining.
|
|
53
|
+
* @example
|
|
54
|
+
* ```
|
|
55
|
+
* queryBuilder.where('status = ?', 'active').where('age > ?', 21);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
25
58
|
where(condition: string, ...params: any[]): QueryBuilder<T> {
|
|
26
59
|
this.whereConditions.push(condition);
|
|
27
60
|
this.whereParams.push(...params);
|
|
28
61
|
return this;
|
|
29
62
|
}
|
|
30
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Adds a LEFT JOIN clause to the query.
|
|
66
|
+
* @param table The name of the table to join with.
|
|
67
|
+
* @param condition The ON condition for the join.
|
|
68
|
+
* @returns The `QueryBuilder` instance for chaining.
|
|
69
|
+
* @example
|
|
70
|
+
* ```
|
|
71
|
+
* queryBuilder.join('profiles', 'profiles.userId = users.id');
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
31
74
|
join(table: string, condition: string): QueryBuilder<T> {
|
|
32
75
|
this.joins.push(`LEFT JOIN ${table} ON ${condition}`);
|
|
33
76
|
return this;
|
|
34
77
|
}
|
|
35
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Adds an ORDER BY clause to the query.
|
|
81
|
+
* @param clause The column and direction for ordering (e.g., 'createdAt DESC').
|
|
82
|
+
* @returns The `QueryBuilder` instance for chaining.
|
|
83
|
+
* @example
|
|
84
|
+
* ```
|
|
85
|
+
* queryBuilder.orderBy('lastName ASC');
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
36
88
|
orderBy(clause: string): QueryBuilder<T> {
|
|
37
89
|
this.orderByClause = clause;
|
|
38
90
|
return this;
|
|
39
91
|
}
|
|
40
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Adds a LIMIT clause to the query to restrict the number of rows returned.
|
|
95
|
+
* @param limit The maximum number of rows to return.
|
|
96
|
+
* @returns The `QueryBuilder` instance for chaining.
|
|
97
|
+
* @example
|
|
98
|
+
* ```
|
|
99
|
+
* queryBuilder.limit(10);
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
41
102
|
limit(limit: number): QueryBuilder<T> {
|
|
42
103
|
this.limitValue = limit;
|
|
43
104
|
return this;
|
|
44
105
|
}
|
|
45
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Adds an OFFSET clause to the query for pagination.
|
|
109
|
+
* @param offset The number of rows to skip.
|
|
110
|
+
* @returns The `QueryBuilder` instance for chaining.
|
|
111
|
+
* @example
|
|
112
|
+
* ```
|
|
113
|
+
* queryBuilder.offset(20);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
46
116
|
offset(offset: number): QueryBuilder<T> {
|
|
47
117
|
this.offsetValue = offset;
|
|
48
118
|
return this;
|
|
49
119
|
}
|
|
50
120
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Constructs the final SQL query string and its corresponding parameters.
|
|
123
|
+
* This is an internal method, typically called by `execute`.
|
|
124
|
+
* @returns An object containing the final `query` string and `params` array.
|
|
125
|
+
*/
|
|
56
126
|
build(): { query: string; params: any[] } {
|
|
57
127
|
let query = `SELECT ${this.selectFields.join(", ")} FROM ${this.table}`;
|
|
58
|
-
|
|
59
|
-
const hintStr = this.hints.map((h) => `${h.type}(${h.value})`).join(" ");
|
|
60
|
-
query = `SELECT ${hintStr} ${this.selectFields.join(", ")} FROM ${this.table}`;
|
|
61
|
-
}
|
|
128
|
+
|
|
62
129
|
if (this.joins.length > 0) {
|
|
63
130
|
query += " " + this.joins.join(" ");
|
|
64
131
|
}
|
|
@@ -77,20 +144,43 @@ export class QueryBuilder<T> {
|
|
|
77
144
|
return { query, params: this.whereParams };
|
|
78
145
|
}
|
|
79
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Executes the constructed query against the database using the provided client.
|
|
149
|
+
* Handles cache-aside logic if a cache and cacheKey are provided.
|
|
150
|
+
* @param client The `DBClient` instance to use for executing the query.
|
|
151
|
+
* @param cache Optional: The `Cache` instance to use for caching.
|
|
152
|
+
* @param cacheKey Optional: The key to use for getting/setting the result in the cache.
|
|
153
|
+
* @returns A promise that resolves to an array of results of type `T`.
|
|
154
|
+
* @example
|
|
155
|
+
* ```
|
|
156
|
+
* const users = await stabilize.getRepository(User)
|
|
157
|
+
* .find()
|
|
158
|
+
* .where('status = ?', 'active')
|
|
159
|
+
* .limit(10)
|
|
160
|
+
* .execute(dbClient, cache, 'active_users_page_1');
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
80
163
|
async execute(
|
|
81
164
|
client: DBClient,
|
|
82
165
|
cache?: Cache,
|
|
83
166
|
cacheKey?: string,
|
|
84
167
|
): Promise<T[]> {
|
|
85
168
|
const { query, params } = this.build();
|
|
169
|
+
|
|
170
|
+
// Attempt to retrieve from cache first (cache-aside read)
|
|
86
171
|
if (cache && cacheKey) {
|
|
87
172
|
const cached = await cache.get<T[]>(cacheKey);
|
|
88
173
|
if (cached) return cached;
|
|
89
174
|
}
|
|
175
|
+
|
|
176
|
+
// If not in cache, execute query against the database
|
|
90
177
|
const results = await client.query<T>(query, params);
|
|
178
|
+
|
|
179
|
+
// Store the database results in the cache for future requests
|
|
91
180
|
if (cache && cacheKey && results.length > 0) {
|
|
92
|
-
await cache.set(cacheKey, results);
|
|
181
|
+
await cache.set(cacheKey, results, 60);
|
|
93
182
|
}
|
|
183
|
+
|
|
94
184
|
return results;
|
|
95
185
|
}
|
|
96
|
-
}
|
|
186
|
+
}
|