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/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
- private client: DBClient;
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
- if (this.cache) {
55
- const cacheInstance = this.cache as any;
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
- async savepoint<T>(name: string, callback: () => Promise<T>): Promise<T> {
79
- return await this.client.savepoint(name, callback);
80
- }
81
-
82
- async switchConnection(config: DBConfig) {
83
- await this.client.switchConnection(config);
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
- return this.cache
88
- ? await this.cache.getStats()
89
- : { hits: 0, misses: 0, keys: 0 };
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) await this.cache.disconnect();
139
+ if (this.cache) {
140
+ await this.cache.disconnect();
141
+ }
99
142
  }
100
143
  }
101
144
 
102
145
  export {
103
- // Types
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
- // src/logger.ts
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 || LogLevel.INFO;
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
- const levels = [
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
- private async rotateLogFile() {
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 && stats.size >= this.maxFileSize) {
49
- for (let i = this.maxFiles - 1; i > 0; i--) {
50
- const oldPath = i === 1 ? this.filePath : `${this.filePath}.${i - 1}`;
51
- const newPath = `${this.filePath}.${i}`;
52
- if (await fs.stat(oldPath).catch(() => null)) {
53
- await fs.rename(oldPath, newPath).catch(() => {});
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
- console.error("Log rotation failed:", 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}`);
61
73
  }
62
74
  }
63
75
 
64
- private async writeToFile(message: string) {
65
- if (!this.filePath) return;
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
- try {
68
- await this.rotateLogFile();
69
- const logEntry = `${new Date().toISOString()} ${message}\n`;
70
- await fs.appendFile(this.filePath, logEntry);
71
- this.currentFileSize += Buffer.byteLength(logEntry);
72
- } catch (error) {
73
- console.error("Failed to write to log file:", error);
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
- logQuery(query: string, params: any[], executionTime?: number) {
78
- if (this.shouldLog(LogLevel.DEBUG)) {
79
- const message = `[DEBUG] Query: ${query} | Params: ${JSON.stringify(params)} | Time: ${executionTime ? `${executionTime.toFixed(2)}ms` : "N/A"}`;
80
- console.log(message);
81
- if (this.filePath) {
82
- this.writeToFile(message);
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
- logError(error: Error) {
88
- if (this.shouldLog(LogLevel.ERROR)) {
89
- const message = `[ERROR] ${error.message}${error.stack ? `\n${error.stack}` : ""}`;
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
- logMetrics(metrics: PoolMetrics) {
98
- if (this.shouldLog(LogLevel.INFO)) {
99
- const message = `[INFO] Pool Metrics: Active=${metrics.activeConnections}, Idle=${metrics.idleConnections}, Total=${metrics.totalConnections}`;
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
- logInfo(message: string) {
108
- if (this.shouldLog(LogLevel.INFO)) {
109
- const formatted = `[INFO] ${message}`;
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
- logDebug(message: string) {
118
- if (this.shouldLog(LogLevel.DEBUG)) {
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
+ }