stabilize-orm 1.1.2 → 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 +160 -57
- package/package.json +5 -2
- package/query-builder.ts +103 -13
- package/repository.ts +447 -287
- package/types.ts +58 -29
package/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @file stabilize.ts
|
|
4
|
+
* @description The main entry point for the Stabilize ORM, tying together the client, cache, and repositories.
|
|
5
|
+
* @author ElectronSz
|
|
6
|
+
*/
|
|
1
7
|
import { Cache } from "./cache";
|
|
2
8
|
import { DBClient } from "./client";
|
|
3
9
|
import { type Logger, ConsoleLogger } from "./logger";
|
|
@@ -33,79 +39,119 @@ import {
|
|
|
33
39
|
LogLevel,
|
|
34
40
|
} from "./types";
|
|
35
41
|
|
|
42
|
+
|
|
36
43
|
export class Stabilize {
|
|
37
|
-
|
|
44
|
+
public client: DBClient;
|
|
38
45
|
private cache: Cache | null;
|
|
39
46
|
private logger: Logger;
|
|
40
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Creates an instance of the Stabilize ORM.
|
|
50
|
+
* @param config The database configuration object.
|
|
51
|
+
* @param cacheConfig Optional configuration for the cache. Caching is disabled if not provided.
|
|
52
|
+
* @param loggerConfig Optional configuration for the logger.
|
|
53
|
+
*/
|
|
41
54
|
constructor(
|
|
42
55
|
config: DBConfig,
|
|
43
56
|
cacheConfig: CacheConfig = { enabled: false, ttl: 60 },
|
|
44
57
|
loggerConfig: LoggerConfig = {},
|
|
58
|
+
existingClient?: DBClient,
|
|
45
59
|
) {
|
|
46
60
|
this.logger = new ConsoleLogger(loggerConfig);
|
|
47
|
-
this.client = new DBClient(config, this.logger);
|
|
48
|
-
this.cache = cacheConfig.enabled
|
|
61
|
+
this.client = existingClient || new DBClient(config, this.logger);
|
|
62
|
+
this.cache = existingClient ? null : (cacheConfig.enabled
|
|
49
63
|
? new Cache(cacheConfig, this.logger)
|
|
50
|
-
: null;
|
|
64
|
+
: null);
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Gets a repository for a given model, used to perform CRUD operations.
|
|
69
|
+
* @param model The model class, which must be decorated with `@Model`.
|
|
70
|
+
* @returns A new `Repository` instance for the specified model.
|
|
71
|
+
* @example
|
|
72
|
+
* ```
|
|
73
|
+
* const stabilize = new Stabilize(dbConfig);
|
|
74
|
+
* const userRepository = stabilize.getRepository(User);
|
|
75
|
+
*
|
|
76
|
+
* const user = await userRepository.findOne(1);
|
|
77
|
+
* console.log(user);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
53
80
|
getRepository<T>(model: new (...args: any[]) => T): Repository<T> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const repoCacheConfig: CacheConfig = {
|
|
57
|
-
enabled: true,
|
|
58
|
-
ttl: 60,
|
|
59
|
-
redisUrl: cacheInstance.redisUrl,
|
|
60
|
-
cachePrefix: cacheInstance.prefix,
|
|
61
|
-
strategy: this.cache.getStrategy(),
|
|
62
|
-
};
|
|
63
|
-
return new Repository(this.client, model, repoCacheConfig, this.logger);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return new Repository(
|
|
67
|
-
this.client,
|
|
68
|
-
model,
|
|
69
|
-
{ enabled: false, ttl: 60 },
|
|
70
|
-
this.logger,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async transaction<T>(callback: () => Promise<T>): Promise<T> {
|
|
75
|
-
return await this.client.transaction(callback);
|
|
81
|
+
const cacheConfig = this.cache ? this.cache.config : undefined;
|
|
82
|
+
return new Repository(this.client, model, cacheConfig, this.logger);
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Executes a callback within a database transaction, ensuring all operations are atomic.
|
|
87
|
+
* The callback receives a transactional `DBClient` instance that must be passed to
|
|
88
|
+
* repository methods to ensure they are part of the same transaction.
|
|
89
|
+
*
|
|
90
|
+
* @param callback The async function to execute. It receives a `txClient` as its only argument.
|
|
91
|
+
* @returns The result of the callback function.
|
|
92
|
+
* @example
|
|
93
|
+
* ```
|
|
94
|
+
* const userRepo = stabilize.getRepository(User);
|
|
95
|
+
* const profileRepo = stabilize.getRepository(Profile);
|
|
96
|
+
*
|
|
97
|
+
* try {
|
|
98
|
+
* await stabilize.transaction(async (txClient) => {
|
|
99
|
+
* const newUser = await userRepo.create({ name: 'Ciniso Dlamini' }, {}, txClient);
|
|
100
|
+
* // The new user's ID is needed for the profile, linking the operations.
|
|
101
|
+
* await profileRepo.create({ userId: newUser.id, bio: 'A new bio' }, {}, txClient);
|
|
102
|
+
* });
|
|
103
|
+
* console.log('User and profile created successfully.');
|
|
104
|
+
* } catch (error) {
|
|
105
|
+
* console.error('Transaction failed, everything was rolled back.', error);
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
async transaction<T>(callback: (txClient: DBClient) => Promise<T>): Promise<T> {
|
|
110
|
+
return this.client.transaction(callback);
|
|
84
111
|
}
|
|
85
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Retrieves statistics from the cache, if it is enabled.
|
|
115
|
+
* @returns A promise that resolves to an object containing cache hits, misses, and total keys.
|
|
116
|
+
* @example
|
|
117
|
+
* ```
|
|
118
|
+
* const stats = await stabilize.getCacheStats();
|
|
119
|
+
* console.log(`Cache Hits: ${stats.hits}, Misses: ${stats.misses}`);
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
86
122
|
async getCacheStats(): Promise<CacheStats> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
getPoolMetrics(): PoolMetrics {
|
|
93
|
-
return this.client.getPoolMetrics();
|
|
123
|
+
if (!this.cache) {
|
|
124
|
+
return { hits: 0, misses: 0, keys: 0 };
|
|
125
|
+
}
|
|
126
|
+
return this.cache.getStats();
|
|
94
127
|
}
|
|
95
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Closes the database connection and disconnects the cache client for a graceful shutdown.
|
|
131
|
+
* @example
|
|
132
|
+
* ```
|
|
133
|
+
* await stabilize.close();
|
|
134
|
+
* console.log('Connections closed.');
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
96
137
|
async close() {
|
|
97
138
|
await this.client.close();
|
|
98
|
-
if (this.cache)
|
|
139
|
+
if (this.cache) {
|
|
140
|
+
await this.cache.disconnect();
|
|
141
|
+
}
|
|
99
142
|
}
|
|
100
143
|
}
|
|
101
144
|
|
|
102
145
|
export {
|
|
103
|
-
|
|
146
|
+
Repository,
|
|
147
|
+
DBClient,
|
|
148
|
+
QueryBuilder,
|
|
149
|
+
Cache,
|
|
150
|
+
ConsoleLogger,
|
|
104
151
|
DBType,
|
|
105
152
|
LogLevel,
|
|
106
153
|
RelationType,
|
|
107
154
|
StabilizeError,
|
|
108
|
-
// Decorators
|
|
109
155
|
Model,
|
|
110
156
|
Column,
|
|
111
157
|
Required,
|
|
@@ -120,19 +166,12 @@ export {
|
|
|
120
166
|
ValidatorKey,
|
|
121
167
|
RelationKey,
|
|
122
168
|
SoftDeleteKey,
|
|
123
|
-
// Classes
|
|
124
|
-
Cache,
|
|
125
|
-
DBClient,
|
|
126
|
-
QueryBuilder,
|
|
127
|
-
Repository,
|
|
128
|
-
ConsoleLogger,
|
|
129
|
-
// Migrations
|
|
130
169
|
runMigrations,
|
|
131
170
|
generateMigration,
|
|
132
171
|
};
|
|
133
172
|
|
|
134
|
-
export type { Migration };
|
|
135
173
|
export type {
|
|
174
|
+
Migration,
|
|
136
175
|
DBConfig,
|
|
137
176
|
CacheConfig,
|
|
138
177
|
LoggerConfig,
|
|
@@ -140,4 +179,4 @@ export type {
|
|
|
140
179
|
PoolMetrics,
|
|
141
180
|
CacheStats,
|
|
142
181
|
Logger,
|
|
143
|
-
};
|
|
182
|
+
};
|
package/logger.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
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
|
+
|
|
2
7
|
import * as fs from "fs/promises";
|
|
3
|
-
import * as path from "path";
|
|
4
8
|
import {
|
|
5
9
|
LogLevel,
|
|
6
10
|
type LoggerConfig,
|
|
@@ -8,119 +12,116 @@ import {
|
|
|
8
12
|
StabilizeError,
|
|
9
13
|
} from "./types";
|
|
10
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Defines the interface for a logger that can be used within the ORM.
|
|
17
|
+
*/
|
|
11
18
|
export interface Logger {
|
|
12
19
|
logQuery(query: string, params: any[], executionTime?: number): void;
|
|
13
20
|
logError(error: Error): void;
|
|
14
21
|
logMetrics(metrics: PoolMetrics): void;
|
|
15
22
|
logInfo(message: string): void;
|
|
23
|
+
logWarn(message: string): void;
|
|
16
24
|
logDebug(message: string): void;
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A logger implementation that writes to the console and can optionally write to rotating files.
|
|
29
|
+
*/
|
|
19
30
|
export class ConsoleLogger implements Logger {
|
|
20
|
-
private level: LogLevel;
|
|
21
|
-
private filePath: string | null;
|
|
22
|
-
private maxFileSize: number;
|
|
23
|
-
private maxFiles: number;
|
|
24
|
-
private currentFileSize: number = 0;
|
|
31
|
+
private readonly level: LogLevel;
|
|
32
|
+
private readonly filePath: string | null;
|
|
33
|
+
private readonly maxFileSize: number;
|
|
34
|
+
private readonly maxFiles: number;
|
|
25
35
|
|
|
26
36
|
constructor(config: LoggerConfig = {}) {
|
|
27
|
-
this.level = config.level
|
|
37
|
+
this.level = config.level ?? LogLevel.Info;
|
|
28
38
|
this.filePath = config.filePath || null;
|
|
29
39
|
this.maxFileSize = config.maxFileSize || 1 * 1024 * 1024; // 1MB
|
|
30
40
|
this.maxFiles = config.maxFiles || 3;
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
/** @internal Checks if a message at a given level should be logged. */
|
|
33
44
|
private shouldLog(messageLevel: LogLevel): boolean {
|
|
34
|
-
|
|
35
|
-
LogLevel.ERROR,
|
|
36
|
-
LogLevel.WARN,
|
|
37
|
-
LogLevel.INFO,
|
|
38
|
-
LogLevel.DEBUG,
|
|
39
|
-
];
|
|
40
|
-
return levels.indexOf(messageLevel) <= levels.indexOf(this.level);
|
|
45
|
+
return messageLevel <= this.level;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
/** @internal Rotates log files if the current one exceeds the max size. */
|
|
49
|
+
private async rotateLogFile(): Promise<void> {
|
|
44
50
|
if (!this.filePath) return;
|
|
45
51
|
|
|
46
52
|
try {
|
|
47
53
|
const stats = await fs.stat(this.filePath).catch(() => null);
|
|
48
|
-
if (stats
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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);
|
|
55
66
|
}
|
|
56
|
-
await fs.writeFile(this.filePath, "");
|
|
57
|
-
this.currentFileSize = 0;
|
|
58
67
|
}
|
|
68
|
+
await fs.rename(this.filePath, `${this.filePath}.1`);
|
|
59
69
|
} catch (error) {
|
|
60
|
-
|
|
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}`);
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
console.
|
|
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;
|
|
74
87
|
}
|
|
75
|
-
}
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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}`);
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.error(message);
|
|
91
|
-
if (this.filePath) {
|
|
92
|
-
this.writeToFile(message);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
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}`);
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.log(message);
|
|
101
|
-
if (this.filePath) {
|
|
102
|
-
this.writeToFile(message);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
106
|
+
public logError(error: Error): void {
|
|
107
|
+
const message = `${error.message}${error.stack ? `\n${error.stack}` : ""}`;
|
|
108
|
+
this.log(LogLevel.Error, message);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log(formatted);
|
|
111
|
-
if (this.filePath) {
|
|
112
|
-
this.writeToFile(formatted);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
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);
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const formatted = `[DEBUG] ${message}`;
|
|
120
|
-
console.log(formatted);
|
|
121
|
-
if (this.filePath) {
|
|
122
|
-
this.writeToFile(formatted);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
116
|
+
public logInfo(message: string): void {
|
|
117
|
+
this.log(LogLevel.Info, message);
|
|
125
118
|
}
|
|
126
|
-
|
|
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
|
+
}
|