stabilize-orm 1.1.3 → 1.1.5

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
+ * @file stabilize.ts
3
+ * @description The main entry point for the Stabilize ORM, tying together the client, cache, and repositories.
4
+ * @author ElectronSz
5
+ * @date 2025-10-15 20:35:34
6
+ */
1
7
  import { Cache } from "./cache";
2
8
  import { DBClient } from "./client";
3
9
  import { type Logger, ConsoleLogger } from "./logger";
@@ -25,6 +31,7 @@ import {
25
31
  type CacheConfig,
26
32
  type LoggerConfig,
27
33
  DBType,
34
+ DataTypes, // --- FIX: Import DataTypes here ---
28
35
  StabilizeError,
29
36
  type PoolMetrics,
30
37
  type QueryHint,
@@ -33,79 +40,120 @@ import {
33
40
  LogLevel,
34
41
  } from "./types";
35
42
 
43
+
36
44
  export class Stabilize {
37
- private client: DBClient;
45
+ public client: DBClient;
38
46
  private cache: Cache | null;
39
47
  private logger: Logger;
40
48
 
49
+ /**
50
+ * Creates an instance of the Stabilize ORM.
51
+ * @param config The database configuration object.
52
+ * @param cacheConfig Optional configuration for the cache. Caching is disabled if not provided.
53
+ * @param loggerConfig Optional configuration for the logger.
54
+ */
41
55
  constructor(
42
56
  config: DBConfig,
43
57
  cacheConfig: CacheConfig = { enabled: false, ttl: 60 },
44
58
  loggerConfig: LoggerConfig = {},
59
+ existingClient?: DBClient,
45
60
  ) {
46
61
  this.logger = new ConsoleLogger(loggerConfig);
47
- this.client = new DBClient(config, this.logger);
48
- this.cache = cacheConfig.enabled
62
+ this.client = existingClient || new DBClient(config, this.logger);
63
+ this.cache = existingClient ? null : (cacheConfig.enabled
49
64
  ? new Cache(cacheConfig, this.logger)
50
- : null;
65
+ : null);
51
66
  }
52
67
 
68
+ /**
69
+ * Gets a repository for a given model, used to perform CRUD operations.
70
+ * @param model The model class, which must be decorated with `@Model`.
71
+ * @returns A new `Repository` instance for the specified model.
72
+ * @example
73
+ * ```
74
+ * const stabilize = new Stabilize(dbConfig);
75
+ * const userRepository = stabilize.getRepository(User);
76
+ *
77
+ * const user = await userRepository.findOne(1);
78
+ * console.log(user);
79
+ * ```
80
+ */
53
81
  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);
82
+ const cacheConfig = this.cache ? this.cache.config : undefined;
83
+ return new Repository(this.client, model, cacheConfig, this.logger);
76
84
  }
77
85
 
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);
86
+ /**
87
+ * Executes a callback within a database transaction, ensuring all operations are atomic.
88
+ * The callback receives a transactional `DBClient` instance that must be passed to
89
+ * repository methods to ensure they are part of the same transaction.
90
+ *
91
+ * @param callback The async function to execute. It receives a `txClient` as its only argument.
92
+ * @returns The result of the callback function.
93
+ * @example
94
+ * ```
95
+ * const userRepo = stabilize.getRepository(User);
96
+ * const profileRepo = stabilize.getRepository(Profile);
97
+ *
98
+ * try {
99
+ * await stabilize.transaction(async (txClient) => {
100
+ * const newUser = await userRepo.create({ name: 'Ciniso Dlamini' }, {}, txClient);
101
+ * // The new user's ID is needed for the profile, linking the operations.
102
+ * await profileRepo.create({ userId: newUser.id, bio: 'A new bio' }, {}, txClient);
103
+ * });
104
+ * console.log('User and profile created successfully.');
105
+ * } catch (error) {
106
+ * console.error('Transaction failed, everything was rolled back.', error);
107
+ * }
108
+ * ```
109
+ */
110
+ async transaction<T>(callback: (txClient: DBClient) => Promise<T>): Promise<T> {
111
+ return this.client.transaction(callback);
84
112
  }
85
113
 
114
+ /**
115
+ * Retrieves statistics from the cache, if it is enabled.
116
+ * @returns A promise that resolves to an object containing cache hits, misses, and total keys.
117
+ * @example
118
+ * ```
119
+ * const stats = await stabilize.getCacheStats();
120
+ * console.log(`Cache Hits: ${stats.hits}, Misses: ${stats.misses}`);
121
+ * ```
122
+ */
86
123
  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();
124
+ if (!this.cache) {
125
+ return { hits: 0, misses: 0, keys: 0 };
126
+ }
127
+ return this.cache.getStats();
94
128
  }
95
129
 
130
+ /**
131
+ * Closes the database connection and disconnects the cache client for a graceful shutdown.
132
+ * @example
133
+ * ```
134
+ * await stabilize.close();
135
+ * console.log('Connections closed.');
136
+ * ```
137
+ */
96
138
  async close() {
97
139
  await this.client.close();
98
- if (this.cache) await this.cache.disconnect();
140
+ if (this.cache) {
141
+ await this.cache.disconnect();
142
+ }
99
143
  }
100
144
  }
101
145
 
102
146
  export {
103
- // Types
147
+ Repository,
148
+ DBClient,
149
+ QueryBuilder,
150
+ Cache,
151
+ ConsoleLogger,
104
152
  DBType,
153
+ DataTypes,
105
154
  LogLevel,
106
155
  RelationType,
107
156
  StabilizeError,
108
- // Decorators
109
157
  Model,
110
158
  Column,
111
159
  Required,
@@ -120,19 +168,12 @@ export {
120
168
  ValidatorKey,
121
169
  RelationKey,
122
170
  SoftDeleteKey,
123
- // Classes
124
- Cache,
125
- DBClient,
126
- QueryBuilder,
127
- Repository,
128
- ConsoleLogger,
129
- // Migrations
130
171
  runMigrations,
131
172
  generateMigration,
132
173
  };
133
174
 
134
- export type { Migration };
135
175
  export type {
176
+ Migration,
136
177
  DBConfig,
137
178
  CacheConfig,
138
179
  LoggerConfig,
@@ -140,4 +181,4 @@ export type {
140
181
  PoolMetrics,
141
182
  CacheStats,
142
183
  Logger,
143
- };
184
+ };
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
+ }