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.
- package/README.md +1097 -546
- package/dist/auto-migrate.d.ts +34 -0
- package/dist/auto-migrate.d.ts.map +1 -0
- package/dist/auto-migrate.js +3003 -0
- package/dist/auto-migrate.js.map +163 -0
- package/dist/cache.d.ts +90 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +166 -0
- package/dist/cache.js.map +64 -0
- package/dist/client.d.ts +73 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2997 -0
- package/dist/client.js.map +162 -0
- package/dist/hooks.d.ts +31 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +4 -0
- package/dist/hooks.js.map +11 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3183 -0
- package/dist/index.js.map +222 -0
- package/dist/logger.d.ts +40 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +8 -0
- package/dist/logger.js.map +11 -0
- package/dist/migrations.d.ts +31 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +3009 -0
- package/dist/migrations.js.map +164 -0
- package/{model.ts → dist/model.d.ts} +124 -189
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +4 -0
- package/dist/model.js.map +10 -0
- package/dist/query-builder.d.ts +91 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +14 -0
- package/dist/query-builder.js.map +12 -0
- package/dist/repository.d.ts +165 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +176 -0
- package/dist/repository.js.map +69 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +10 -0
- package/dist/utils/encryption.d.ts +13 -0
- package/dist/utils/encryption.d.ts.map +1 -0
- package/dist/utils/encryption.js +4 -0
- package/dist/utils/encryption.js.map +10 -0
- package/package.json +104 -25
- package/.eslintrc.json +0 -10
- package/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -25
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- package/.github/workflows/ci-cd.yml +0 -22
- package/CHANGELOG.md +0 -75
- package/CODE_OF_CONDUCT.md +0 -87
- package/CONTRIBUTING.md +0 -48
- package/FUNDING.md +0 -14
- package/SECURITY.md +0 -35
- package/SUPPORT.md +0 -18
- package/bun.lock +0 -667
- package/cache.ts +0 -181
- package/client.ts +0 -249
- package/docker-compose.yml +0 -22
- package/hooks.ts +0 -76
- package/index.ts +0 -158
- package/logger.ts +0 -127
- package/migrations.ts +0 -318
- package/query-builder.ts +0 -209
- package/repository.ts +0 -1096
- package/tests/migrations.test.ts +0 -141
- package/tsconfig.json +0 -32
- package/types.ts +0 -106
package/cache.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file cache.ts
|
|
3
|
-
* @description Provides a Redis-backed caching layer for the ORM.
|
|
4
|
-
* @author ElectronSz
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import Redis from "ioredis";
|
|
8
|
-
import { type CacheConfig, type CacheStats } from "./types";
|
|
9
|
-
import { StabilizeLogger, type Logger } from "./logger";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A caching client that uses Redis to store and retrieve query results.
|
|
13
|
-
* It supports cache-aside and write-through strategies and keeps track of basic stats.
|
|
14
|
-
*/
|
|
15
|
-
export class Cache {
|
|
16
|
-
private redis: Redis | null = null;
|
|
17
|
-
private logger: Logger;
|
|
18
|
-
private hits: number = 0;
|
|
19
|
-
private misses: number = 0;
|
|
20
|
-
|
|
21
|
-
/** The configuration object the cache was initialized with. */
|
|
22
|
-
public readonly config: CacheConfig;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Creates an instance of the Cache client.
|
|
26
|
-
* @param config The configuration for the cache, including Redis URL and TTL.
|
|
27
|
-
* @param logger A logger instance for logging messages.
|
|
28
|
-
*/
|
|
29
|
-
constructor(config: CacheConfig, logger: Logger = new StabilizeLogger()) {
|
|
30
|
-
this.config = config;
|
|
31
|
-
this.logger = logger;
|
|
32
|
-
|
|
33
|
-
if (this.config.enabled && this.config.redisUrl) {
|
|
34
|
-
this.redis = new Redis(this.config.redisUrl, { lazyConnect: true });
|
|
35
|
-
this.redis.on("error", (error) => this.logger.logError(error));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Gets the caching strategy being used.
|
|
41
|
-
* @returns The caching strategy, either 'cache-aside' or 'write-through'.
|
|
42
|
-
*/
|
|
43
|
-
getStrategy() {
|
|
44
|
-
return this.config.strategy || "cache-aside";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Retrieves an item from the cache.
|
|
49
|
-
* @template T The expected type of the cached item.
|
|
50
|
-
* @param key The key of the item to retrieve.
|
|
51
|
-
* @returns A promise that resolves to the cached item or `null` if not found.
|
|
52
|
-
* @example
|
|
53
|
-
* ```
|
|
54
|
-
* const user = await cache.get<User>('user:1');
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
async get<T>(key: string): Promise<T | null> {
|
|
58
|
-
if (!this.redis) return null;
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const data = await this.redis.get(this.config.cachePrefix + key);
|
|
62
|
-
if (data) {
|
|
63
|
-
this.hits++;
|
|
64
|
-
this.logger.logDebug(`Cache hit for key: ${key}`);
|
|
65
|
-
return JSON.parse(data) as T;
|
|
66
|
-
}
|
|
67
|
-
this.misses++;
|
|
68
|
-
this.logger.logDebug(`Cache miss for key: ${key}`);
|
|
69
|
-
return null;
|
|
70
|
-
} catch (error) {
|
|
71
|
-
this.logger.logError(error as Error);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Stores an item in the cache.
|
|
78
|
-
* @template T The type of the item being stored.
|
|
79
|
-
* @param key The key to store the item under.
|
|
80
|
-
* @param value The value to store.
|
|
81
|
-
* @param ttl Optional: The time-to-live for this specific item in seconds. Defaults to the global TTL.
|
|
82
|
-
* @returns A promise that resolves when the item is set.
|
|
83
|
-
* @example
|
|
84
|
-
* ```
|
|
85
|
-
* await cache.set('user:1', user, 3600); // Cache for 1 hour
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
89
|
-
if (!this.redis) return;
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const effectiveTtl = ttl ?? this.config.ttl;
|
|
93
|
-
await this.redis.set(this.config.cachePrefix + key, JSON.stringify(value), "EX", effectiveTtl);
|
|
94
|
-
this.logger.logDebug(`Cache set for key: ${key}`);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
this.logger.logError(error as Error);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Removes one or more items from the cache by their exact keys.
|
|
102
|
-
* @param keys An array of keys to invalidate.
|
|
103
|
-
* @returns A promise that resolves when the keys are invalidated.
|
|
104
|
-
* @example
|
|
105
|
-
* ```
|
|
106
|
-
* await cache.invalidate(['user:1', 'all_users']);
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
async invalidate(keys: string[]): Promise<void> {
|
|
110
|
-
if (!this.redis) return;
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const pipeline = this.redis.pipeline();
|
|
114
|
-
for (const key of keys) {
|
|
115
|
-
pipeline.del(this.config.cachePrefix + key);
|
|
116
|
-
this.logger.logDebug(`Cache invalidated for key: ${key}`);
|
|
117
|
-
}
|
|
118
|
-
await pipeline.exec();
|
|
119
|
-
} catch (error) {
|
|
120
|
-
this.logger.logError(error as Error);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Invalidates all keys matching a given pattern.
|
|
126
|
-
* @param pattern The pattern to match against (e.g., 'user:*').
|
|
127
|
-
* @returns A promise that resolves when the operation is complete.
|
|
128
|
-
* @example
|
|
129
|
-
* ```
|
|
130
|
-
* await cache.invalidatePattern('user:*'); // Invalidates all user-related cache
|
|
131
|
-
* ```
|
|
132
|
-
*/
|
|
133
|
-
async invalidatePattern(pattern: string): Promise<void> {
|
|
134
|
-
if (!this.redis) return;
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const keys = await this.redis.keys(this.config.cachePrefix + pattern);
|
|
138
|
-
if (keys.length > 0) {
|
|
139
|
-
const pipeline = this.redis.pipeline();
|
|
140
|
-
for (const key of keys) {
|
|
141
|
-
pipeline.del(key);
|
|
142
|
-
}
|
|
143
|
-
await pipeline.exec();
|
|
144
|
-
this.logger.logDebug(`Cache invalidated for pattern: ${pattern} (${keys.length} keys)`);
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
this.logger.logError(error as Error);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Retrieves statistics about the cache, including hits, misses, and total key count.
|
|
153
|
-
* @returns A promise that resolves to a `CacheStats` object.
|
|
154
|
-
* @example
|
|
155
|
-
* ```
|
|
156
|
-
* const stats = await cache.getStats();
|
|
157
|
-
* console.log(`Cache Hits: ${stats.hits}, Misses: ${stats.misses}`);
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
|
-
async getStats(): Promise<CacheStats> {
|
|
161
|
-
if (!this.redis) return { hits: 0, misses: 0, keys: 0 };
|
|
162
|
-
try {
|
|
163
|
-
const keys = await this.redis.keys(this.config.cachePrefix + "*");
|
|
164
|
-
return { hits: this.hits, misses: this.misses, keys: keys.length };
|
|
165
|
-
} catch (error) {
|
|
166
|
-
this.logger.logError(error as Error);
|
|
167
|
-
return { hits: this.hits, misses: this.misses, keys: 0 };
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Disconnects the Redis client gracefully.
|
|
173
|
-
* @returns A promise that resolves when the client has disconnected.
|
|
174
|
-
*/
|
|
175
|
-
async disconnect(): Promise<void> {
|
|
176
|
-
if (this.redis) {
|
|
177
|
-
await this.redis.quit();
|
|
178
|
-
this.logger.logInfo("Redis connection closed");
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
package/client.ts
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file client.ts
|
|
3
|
-
* @description Provides a unified database client for interacting with PostgreSQL, MySQL, and SQLite.
|
|
4
|
-
* @author ElectronSz
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Database, Statement } from "bun:sqlite";
|
|
8
|
-
import { Pool, type PoolClient } from "pg";
|
|
9
|
-
import mysql from "mysql2/promise";
|
|
10
|
-
import {
|
|
11
|
-
type DBConfig,
|
|
12
|
-
StabilizeError,
|
|
13
|
-
DBType,
|
|
14
|
-
} from "./types";
|
|
15
|
-
import { type Logger, StabilizeLogger } from "./logger";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Checks if the DB configuration is for SQLite.
|
|
19
|
-
* @param config The database configuration object.
|
|
20
|
-
* @returns True if the configuration is for SQLite, false otherwise.
|
|
21
|
-
*/
|
|
22
|
-
function isSQLiteConfig(config: DBConfig): boolean {
|
|
23
|
-
return config.type === DBType.SQLite;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Checks if the DB configuration is for MySQL.
|
|
28
|
-
* @param config The database configuration object.
|
|
29
|
-
* @returns True if the configuration is for MySQL, false otherwise.
|
|
30
|
-
*/
|
|
31
|
-
function isMySQLConfig(config: DBConfig): boolean {
|
|
32
|
-
return config.type === DBType.MySQL;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Checks if the given client is a MySQL pool.
|
|
37
|
-
* @param client The database client.
|
|
38
|
-
* @returns True if the client is a MySQL pool, false otherwise.
|
|
39
|
-
*/
|
|
40
|
-
function isMySQLPool(client: any): client is mysql.Pool {
|
|
41
|
-
return typeof client.getConnection === 'function';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Provides a unified database client for interacting with PostgreSQL, MySQL, and SQLite.
|
|
46
|
-
*/
|
|
47
|
-
export class DBClient {
|
|
48
|
-
private client!: Database | Pool | mysql.Pool | PoolClient | mysql.PoolConnection;
|
|
49
|
-
private logger: Logger;
|
|
50
|
-
public readonly config: DBConfig;
|
|
51
|
-
private retryAttempts: number;
|
|
52
|
-
private retryDelay: number;
|
|
53
|
-
private maxJitter: number;
|
|
54
|
-
|
|
55
|
-
private preparedStatements: Map<string, Statement> = new Map();
|
|
56
|
-
public readonly isTransactionClient: boolean = false;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Constructs a new DBClient instance.
|
|
60
|
-
* @param config The database configuration object.
|
|
61
|
-
* @param logger Optional logger instance. Uses StabilizeLogger if not provided.
|
|
62
|
-
* @param existingClient Optional existing transaction client.
|
|
63
|
-
*/
|
|
64
|
-
constructor(
|
|
65
|
-
config: DBConfig,
|
|
66
|
-
logger: Logger = new StabilizeLogger(),
|
|
67
|
-
existingClient: PoolClient | mysql.PoolConnection | null = null,
|
|
68
|
-
) {
|
|
69
|
-
this.config = config;
|
|
70
|
-
this.logger = logger;
|
|
71
|
-
this.retryAttempts = config.retryAttempts || 3;
|
|
72
|
-
this.retryDelay = config.retryDelay || 1000;
|
|
73
|
-
this.maxJitter = config.maxJitter || 100;
|
|
74
|
-
|
|
75
|
-
if (existingClient) {
|
|
76
|
-
this.client = existingClient;
|
|
77
|
-
this.isTransactionClient = true;
|
|
78
|
-
} else {
|
|
79
|
-
this.initializeClient(config);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Initializes the database client based on the configuration.
|
|
85
|
-
* @param config The database configuration object.
|
|
86
|
-
*/
|
|
87
|
-
private initializeClient(config: DBConfig) {
|
|
88
|
-
if (isSQLiteConfig(config)) {
|
|
89
|
-
this.client = new Database(config.connectionString, { create: true });
|
|
90
|
-
this.logger.logDebug(`Initialized Bun SQLite client.`);
|
|
91
|
-
} else if (isMySQLConfig(config)) {
|
|
92
|
-
this.client = mysql.createPool(config.connectionString);
|
|
93
|
-
this.logger.logDebug(`Initialized MySQL Pool client.`);
|
|
94
|
-
} else if (config.type = DBType.Postgres) { // NOTE: single '=' should be '===', this is likely a bug
|
|
95
|
-
this.client = new Pool({ connectionString: config.connectionString! });
|
|
96
|
-
this.logger.logDebug(`Initialized Postgres Pool client.`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Returns a random jitter value for retry logic.
|
|
102
|
-
* @returns A random number up to maxJitter.
|
|
103
|
-
*/
|
|
104
|
-
private getJitter = () => Math.random() * this.maxJitter;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Executes a SQL query with retries and returns the resulting rows.
|
|
108
|
-
* @param query The SQL query string.
|
|
109
|
-
* @param params Query parameters.
|
|
110
|
-
* @returns Array of resulting rows.
|
|
111
|
-
* @throws StabilizeError if all retry attempts fail.
|
|
112
|
-
*/
|
|
113
|
-
async query<T>(query: string, params: any[] = []): Promise<T[]> {
|
|
114
|
-
const start = Date.now();
|
|
115
|
-
|
|
116
|
-
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
117
|
-
try {
|
|
118
|
-
let result: any;
|
|
119
|
-
|
|
120
|
-
if (this.client instanceof Database) {
|
|
121
|
-
let stmt = this.preparedStatements.get(query);
|
|
122
|
-
if (!stmt) {
|
|
123
|
-
stmt = this.client.prepare(query);
|
|
124
|
-
this.preparedStatements.set(query, stmt);
|
|
125
|
-
}
|
|
126
|
-
result = stmt.all(...params);
|
|
127
|
-
} else if (this.config.type === DBType.MySQL) {
|
|
128
|
-
const [rows] = await (this.client as mysql.Pool).query(query, params);
|
|
129
|
-
result = rows;
|
|
130
|
-
} else if (this.config.type === DBType.Postgres ) {
|
|
131
|
-
let paramIndex = 0;
|
|
132
|
-
const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
|
|
133
|
-
const pgResult = await (this.client as Pool).query(pgQuery, params);
|
|
134
|
-
result = Array.isArray(pgResult.rows) ? pgResult.rows : [];
|
|
135
|
-
} else {
|
|
136
|
-
throw new StabilizeError("Unknown database client type", "QUERY_ERROR");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const executionTime = Date.now() - start;
|
|
140
|
-
this.logger.logQuery(query, params, executionTime);
|
|
141
|
-
return Array.isArray(result) ? result as T[] : [];
|
|
142
|
-
} catch (error) {
|
|
143
|
-
this.logger.logError(error as Error);
|
|
144
|
-
if (attempt === this.retryAttempts) {
|
|
145
|
-
throw new StabilizeError(`Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`, "QUERY_ERROR");
|
|
146
|
-
}
|
|
147
|
-
await new Promise(res => setTimeout(res, this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter()));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
throw new StabilizeError("Query failed: maximum retries reached without success", "QUERY_ERROR");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Runs a callback within a database transaction.
|
|
155
|
-
* Handles commit/rollback and connection release.
|
|
156
|
-
* @param callback The callback to execute within the transaction context.
|
|
157
|
-
* @returns The result of the callback.
|
|
158
|
-
* @throws StabilizeError if transactions are not supported or rollback is triggered.
|
|
159
|
-
*/
|
|
160
|
-
async transaction<T>(callback: (txClient: DBClient) => Promise<T>): Promise<T> {
|
|
161
|
-
if (this.isTransactionClient) return callback(this);
|
|
162
|
-
|
|
163
|
-
if (this.client instanceof Database) {
|
|
164
|
-
const tx = this.client.transaction(() => callback(this));
|
|
165
|
-
return tx();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (isMySQLPool(this.client)) {
|
|
169
|
-
const connection = await this.client.getConnection();
|
|
170
|
-
const txClient = new DBClient(this.config, this.logger, connection);
|
|
171
|
-
this.logger.logDebug("Starting MySQL transaction.");
|
|
172
|
-
try {
|
|
173
|
-
await txClient.query("START TRANSACTION");
|
|
174
|
-
const result = await callback(txClient);
|
|
175
|
-
await txClient.query("COMMIT");
|
|
176
|
-
return result;
|
|
177
|
-
} catch (error) {
|
|
178
|
-
await txClient.query("ROLLBACK");
|
|
179
|
-
throw error;
|
|
180
|
-
} finally {
|
|
181
|
-
connection.release();
|
|
182
|
-
this.logger.logDebug("MySQL transaction connection released.");
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (this.client instanceof Pool) {
|
|
187
|
-
const connection = await this.client.connect();
|
|
188
|
-
const txClient = new DBClient(this.config, this.logger, connection);
|
|
189
|
-
this.logger.logDebug("Starting Postgres transaction.");
|
|
190
|
-
try {
|
|
191
|
-
await txClient.migrationQuery("BEGIN");
|
|
192
|
-
const result = await callback(txClient);
|
|
193
|
-
await txClient.migrationQuery("COMMIT");
|
|
194
|
-
return result;
|
|
195
|
-
} catch (error) {
|
|
196
|
-
await txClient.migrationQuery("ROLLBACK");
|
|
197
|
-
throw error;
|
|
198
|
-
} finally {
|
|
199
|
-
connection.release();
|
|
200
|
-
this.logger.logDebug("Postgres transaction connection released.");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
throw new StabilizeError("Transaction not supported by this client configuration.", "TX_ERROR");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Closes the database connection.
|
|
209
|
-
* For pooled connections, ends the pool.
|
|
210
|
-
* @returns Promise that resolves once the connection is closed.
|
|
211
|
-
*/
|
|
212
|
-
async close() {
|
|
213
|
-
if (this.client instanceof Database) {
|
|
214
|
-
this.client.close();
|
|
215
|
-
} else if (this.client && 'end' in this.client) {
|
|
216
|
-
await (this.client as any).end();
|
|
217
|
-
}
|
|
218
|
-
this.client = null!;
|
|
219
|
-
this.logger.logInfo("Database connection closed");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Executes a migration query (DDL or DML statement) without returning results.
|
|
224
|
-
* Handles parameterized queries and statement preparation.
|
|
225
|
-
* @param query The SQL query string.
|
|
226
|
-
* @param params Query parameters.
|
|
227
|
-
* @returns Promise that resolves once the query is complete.
|
|
228
|
-
*/
|
|
229
|
-
async migrationQuery(query: string, params: any[] = []): Promise<void> {
|
|
230
|
-
const start = Date.now();
|
|
231
|
-
if (this.client instanceof Database) {
|
|
232
|
-
let stmt = this.preparedStatements.get(query);
|
|
233
|
-
if (!stmt) {
|
|
234
|
-
stmt = this.client.prepare(query);
|
|
235
|
-
this.preparedStatements.set(query, stmt);
|
|
236
|
-
}
|
|
237
|
-
stmt.run(...params);
|
|
238
|
-
} else if (isMySQLPool(this.client) || ('query' in this.client && 'release' in this.client && !(this.client instanceof Pool))) {
|
|
239
|
-
await (this.client as mysql.Pool).query(query, params);
|
|
240
|
-
} else if (this.config.type = DBType.Postgres) { // NOTE: single '=' should be '===', this is likely a bug
|
|
241
|
-
let paramIndex = 0;
|
|
242
|
-
const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
|
|
243
|
-
await (this.client as Pool).query(pgQuery, params);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const executionTime = Date.now() - start;
|
|
247
|
-
this.logger.logQuery(query, params, executionTime);
|
|
248
|
-
}
|
|
249
|
-
}
|
package/docker-compose.yml
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
services:
|
|
3
|
-
db:
|
|
4
|
-
image: mariadb:latest
|
|
5
|
-
container_name: mariadb-container
|
|
6
|
-
restart: always
|
|
7
|
-
environment:
|
|
8
|
-
# Required: set a secure root password
|
|
9
|
-
MARIADB_ROOT_PASSWORD: P@ssw0rd
|
|
10
|
-
# Optional: set up a default database and user
|
|
11
|
-
MARIADB_DATABASE: db
|
|
12
|
-
MARIADB_USER: admin
|
|
13
|
-
MARIADB_PASSWORD: P@ssw0rd
|
|
14
|
-
ports:
|
|
15
|
-
# Map host port 3306 to container port 3306
|
|
16
|
-
- '3306:3306'
|
|
17
|
-
volumes:
|
|
18
|
-
# Create a named volume for persistent data storage
|
|
19
|
-
- mariadb_data:/var/lib/mysql
|
|
20
|
-
|
|
21
|
-
volumes:
|
|
22
|
-
mariadb_data:
|
package/hooks.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file hooks.ts
|
|
3
|
-
* @description Provides lifecycle hooks for Stabilize ORM models, integrated with the programmatic API.
|
|
4
|
-
* @author ElectronSz
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { MetadataStorage } from "./model";
|
|
8
|
-
|
|
9
|
-
export type HookType =
|
|
10
|
-
| "beforeCreate"
|
|
11
|
-
| "afterCreate"
|
|
12
|
-
| "beforeUpdate"
|
|
13
|
-
| "afterUpdate"
|
|
14
|
-
| "beforeSave"
|
|
15
|
-
| "afterSave"
|
|
16
|
-
| "beforeDelete"
|
|
17
|
-
| "afterDelete";
|
|
18
|
-
|
|
19
|
-
export type HookCallback = (entity: any) => Promise<void> | void;
|
|
20
|
-
|
|
21
|
-
export interface Hook {
|
|
22
|
-
type: HookType;
|
|
23
|
-
callback: HookCallback;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Extend ModelConfig to include hooks
|
|
27
|
-
declare module "./model" {
|
|
28
|
-
interface ModelConfig {
|
|
29
|
-
hooks?: Partial<Record<HookType, HookCallback | HookCallback[]>>;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Registers hooks for a model in the MetadataStorage.
|
|
35
|
-
* @param model The model class.
|
|
36
|
-
* @param hooks A record of hook types to their callbacks.
|
|
37
|
-
*/
|
|
38
|
-
export function registerHooks(model: Function, hooks: Record<HookType, HookCallback | HookCallback[]>) {
|
|
39
|
-
const config = MetadataStorage.getModelMetadata(model) || { tableName: "", columns: {} };
|
|
40
|
-
config.hooks = { ...config.hooks, ...hooks };
|
|
41
|
-
MetadataStorage.setModelMetadata(model, config);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Retrieves hooks for a given entity and hook type.
|
|
46
|
-
* Combines hooks from MetadataStorage and class methods.
|
|
47
|
-
* @param entity The entity instance.
|
|
48
|
-
* @param type The hook type (e.g., 'beforeCreate').
|
|
49
|
-
* @returns An array of Hook objects to execute.
|
|
50
|
-
*/
|
|
51
|
-
export function getHooks(entity: any, type: HookType): Hook[] {
|
|
52
|
-
const hooks: Hook[] = [];
|
|
53
|
-
const model = Object.getPrototypeOf(entity).constructor;
|
|
54
|
-
|
|
55
|
-
// Get hooks from MetadataStorage
|
|
56
|
-
const config = MetadataStorage.getModelMetadata(model);
|
|
57
|
-
if (config?.hooks?.[type]) {
|
|
58
|
-
const callbacks = Array.isArray(config.hooks[type])
|
|
59
|
-
? config.hooks[type]
|
|
60
|
-
: [config.hooks[type]];
|
|
61
|
-
hooks.push(...callbacks.map(callback => ({
|
|
62
|
-
type,
|
|
63
|
-
callback: () => callback(entity),
|
|
64
|
-
})));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Get hooks from class methods
|
|
68
|
-
if (typeof entity[type] === "function") {
|
|
69
|
-
hooks.push({
|
|
70
|
-
type,
|
|
71
|
-
callback: () => entity[type](),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return hooks;
|
|
76
|
-
}
|
package/index.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file stabilize.ts
|
|
3
|
-
* @description The main entry point for the Stabilize ORM, tying together the client, cache, and repositories.
|
|
4
|
-
* @author ElectronSz
|
|
5
|
-
*/
|
|
6
|
-
import { Cache } from "./cache";
|
|
7
|
-
import { DBClient } from "./client";
|
|
8
|
-
import { type Logger, StabilizeLogger } from "./logger";
|
|
9
|
-
import { QueryBuilder } from "./query-builder";
|
|
10
|
-
import { Repository } from "./repository";
|
|
11
|
-
import { runMigrations, generateMigration, type Migration, mapDataTypeToSql} from "./migrations";
|
|
12
|
-
import {
|
|
13
|
-
type DBConfig,
|
|
14
|
-
type CacheConfig,
|
|
15
|
-
type LoggerConfig,
|
|
16
|
-
DBType,
|
|
17
|
-
DataTypes,
|
|
18
|
-
StabilizeError,
|
|
19
|
-
type PoolMetrics,
|
|
20
|
-
type QueryHint,
|
|
21
|
-
RelationType,
|
|
22
|
-
type CacheStats,
|
|
23
|
-
LogLevel,
|
|
24
|
-
} from "./types";
|
|
25
|
-
import { defineModel, MetadataStorage } from "./model";
|
|
26
|
-
import type { Hook } from "./hooks";
|
|
27
|
-
|
|
28
|
-
export class Stabilize {
|
|
29
|
-
public client: DBClient;
|
|
30
|
-
private cache: Cache | null;
|
|
31
|
-
private logger: Logger;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Creates an instance of the Stabilize ORM.
|
|
35
|
-
* @param config The database configuration object.
|
|
36
|
-
* @param cacheConfig Optional configuration for the cache. Caching is disabled if not provided.
|
|
37
|
-
* @param loggerConfig Optional configuration for the logger.
|
|
38
|
-
*/
|
|
39
|
-
constructor(
|
|
40
|
-
config: DBConfig,
|
|
41
|
-
cacheConfig: CacheConfig = { enabled: false, ttl: 60 },
|
|
42
|
-
loggerConfig: LoggerConfig = {},
|
|
43
|
-
existingClient?: DBClient,
|
|
44
|
-
) {
|
|
45
|
-
this.logger = new StabilizeLogger(loggerConfig);
|
|
46
|
-
this.client = existingClient || new DBClient(config, this.logger);
|
|
47
|
-
this.cache = existingClient ? null : (cacheConfig.enabled
|
|
48
|
-
? new Cache(cacheConfig, this.logger)
|
|
49
|
-
: null);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Gets a repository for a given model, used to perform CRUD operations.
|
|
54
|
-
* @param model The model class, defined using `defineModel`.
|
|
55
|
-
* @returns A new `Repository` instance for the specified model.
|
|
56
|
-
* @example
|
|
57
|
-
* ```
|
|
58
|
-
* const stabilize = new Stabilize(dbConfig);
|
|
59
|
-
* const userRepository = stabilize.getRepository(User);
|
|
60
|
-
*
|
|
61
|
-
* const user = await userRepository.findOne(1);
|
|
62
|
-
* console.log(user);
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
getRepository<T>(model: new (...args: any[]) => T): Repository<T> {
|
|
66
|
-
const cacheConfig = this.cache ? this.cache.config : undefined;
|
|
67
|
-
return new Repository(this.client, model, cacheConfig, this.logger);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Executes a callback within a database transaction, ensuring all operations are atomic.
|
|
72
|
-
* The callback receives a transactional `DBClient` instance that must be passed to
|
|
73
|
-
* repository methods to ensure they are part of the same transaction.
|
|
74
|
-
*
|
|
75
|
-
* @param callback The async function to execute. It receives a `txClient` as its only argument.
|
|
76
|
-
* @returns The result of the callback function.
|
|
77
|
-
* @example
|
|
78
|
-
* ```
|
|
79
|
-
* const userRepo = stabilize.getRepository(User);
|
|
80
|
-
* const profileRepo = stabilize.getRepository(Profile);
|
|
81
|
-
*
|
|
82
|
-
* try {
|
|
83
|
-
* await stabilize.transaction(async (txClient) => {
|
|
84
|
-
* const newUser = await userRepo.create({ name: 'Ciniso Dlamini' }, {}, txClient);
|
|
85
|
-
* await profileRepo.create({ userId: newUser.id, bio: 'A new bio' }, {}, txClient);
|
|
86
|
-
* });
|
|
87
|
-
* console.log('User and profile created successfully.');
|
|
88
|
-
* } catch (error) {
|
|
89
|
-
* console.error('Transaction failed, everything was rolled back.', error);
|
|
90
|
-
* }
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
async transaction<T>(callback: (txClient: DBClient) => Promise<T>): Promise<T> {
|
|
94
|
-
return this.client.transaction(callback);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Retrieves statistics from the cache, if it is enabled.
|
|
99
|
-
* @returns A promise that resolves to an object containing cache hits, misses, and total keys.
|
|
100
|
-
* @example
|
|
101
|
-
* ```
|
|
102
|
-
* const stats = await stabilize.getCacheStats();
|
|
103
|
-
* console.log(`Cache Hits: ${stats.hits}, Misses: ${stats.misses}`);
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
async getCacheStats(): Promise<CacheStats> {
|
|
107
|
-
if (!this.cache) {
|
|
108
|
-
return { hits: 0, misses: 0, keys: 0 };
|
|
109
|
-
}
|
|
110
|
-
return this.cache.getStats();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Closes the database connection and disconnects the cache client for a graceful shutdown.
|
|
115
|
-
* @example
|
|
116
|
-
* ```
|
|
117
|
-
* await stabilize.close();
|
|
118
|
-
* console.log('Connections closed.');
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
async close() {
|
|
122
|
-
await this.client.close();
|
|
123
|
-
if (this.cache) {
|
|
124
|
-
await this.cache.disconnect();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export {
|
|
130
|
-
Repository,
|
|
131
|
-
DBClient,
|
|
132
|
-
QueryBuilder,
|
|
133
|
-
Cache,
|
|
134
|
-
StabilizeLogger,
|
|
135
|
-
DBType,
|
|
136
|
-
DataTypes,
|
|
137
|
-
LogLevel,
|
|
138
|
-
RelationType,
|
|
139
|
-
MetadataStorage,
|
|
140
|
-
mapDataTypeToSql,
|
|
141
|
-
StabilizeError,
|
|
142
|
-
runMigrations,
|
|
143
|
-
generateMigration,
|
|
144
|
-
defineModel,
|
|
145
|
-
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export type {
|
|
149
|
-
Migration,
|
|
150
|
-
DBConfig,
|
|
151
|
-
CacheConfig,
|
|
152
|
-
LoggerConfig,
|
|
153
|
-
QueryHint,
|
|
154
|
-
PoolMetrics,
|
|
155
|
-
CacheStats,
|
|
156
|
-
Logger,
|
|
157
|
-
Hook
|
|
158
|
-
};
|