stabilize-orm 1.0.6 → 1.0.7
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 +1 -1
- package/dist/stabilize-cli.exe +0 -0
- package/package.json +25 -12
- package/.eslintrc.json +0 -10
- package/.github/workflows/ci-cd.yml +0 -73
- package/bun.lock +0 -605
- package/cli/stabilize-cli.ts +0 -401
- package/examples/config/dataase.ts +0 -8
- package/examples/migrations/20251013_seed_history.ts +0 -0
- package/examples/models/Role.ts +0 -11
- package/examples/models/User.ts +0 -22
- package/examples/seeds/20251013_additional_seed.ts +0 -29
- package/examples/seeds/20251013_initial_seed.ts +0 -33
- package/src/LICENSE +0 -21
- package/src/cache.ts +0 -110
- package/src/client.ts +0 -258
- package/src/decorators.ts +0 -99
- package/src/index.ts +0 -143
- package/src/logger.ts +0 -126
- package/src/migrations.ts +0 -81
- package/src/query-builder.ts +0 -96
- package/src/repository.ts +0 -565
- package/src/types.ts +0 -76
- package/tests/migrations.test.ts +0 -141
- package/tsconfig.json +0 -32
package/src/client.ts
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { sql, SQL } from "bun";
|
|
2
|
-
import { Database, Statement } from "bun:sqlite";
|
|
3
|
-
import {
|
|
4
|
-
type DBConfig,
|
|
5
|
-
StabilizeError,
|
|
6
|
-
type PoolMetrics,
|
|
7
|
-
DBType, // Added DBType for type checking
|
|
8
|
-
type LoggerConfig, // Kept, but not used in logic
|
|
9
|
-
} from "./types";
|
|
10
|
-
import { type Logger, ConsoleLogger } from "./logger";
|
|
11
|
-
|
|
12
|
-
// Helper to determine if we are in Bun SQLite mode based on config
|
|
13
|
-
function isSQLiteConfig(config: DBConfig): boolean {
|
|
14
|
-
return (
|
|
15
|
-
config.type === DBType.SQLite || config.connectionString.includes("sqlite")
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class DBClient {
|
|
20
|
-
// Renamed to 'client' for clarity; stores the actual connection (Bun.Database or external driver)
|
|
21
|
-
// We use 'any' for the SQL client instance since Bun's SQL client is complex (callable function with methods)
|
|
22
|
-
private client: Database | any | null = null;
|
|
23
|
-
private logger: Logger;
|
|
24
|
-
private retryAttempts: number;
|
|
25
|
-
private retryDelay: number;
|
|
26
|
-
private maxJitter: number;
|
|
27
|
-
// Map stores Bun SQLite Statement objects when in SQLite mode
|
|
28
|
-
private preparedStatements: Map<string, Statement> = new Map();
|
|
29
|
-
private config: DBConfig;
|
|
30
|
-
|
|
31
|
-
constructor(config: DBConfig, logger: Logger = new ConsoleLogger()) {
|
|
32
|
-
this.config = config;
|
|
33
|
-
this.logger = logger;
|
|
34
|
-
this.retryAttempts = config.retryAttempts || 3;
|
|
35
|
-
this.retryDelay = config.retryDelay || 1000;
|
|
36
|
-
this.maxJitter = config.maxJitter || 100;
|
|
37
|
-
|
|
38
|
-
this.initializeClient(config);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Initializes the connection based on config
|
|
42
|
-
private initializeClient(config: DBConfig) {
|
|
43
|
-
if (isSQLiteConfig(config)) {
|
|
44
|
-
// Use Bun's native SQLite client, which is synchronous to construct
|
|
45
|
-
try {
|
|
46
|
-
// new Database(path, { create: true }) is the standard Bun method
|
|
47
|
-
this.client = new Database(config.connectionString, { create: true });
|
|
48
|
-
this.logger.logDebug(
|
|
49
|
-
`Initialized Bun SQLite client for: ${config.connectionString}`,
|
|
50
|
-
);
|
|
51
|
-
} catch (e) {
|
|
52
|
-
this.logger.logError(e as Error);
|
|
53
|
-
throw new StabilizeError(
|
|
54
|
-
`Failed to initialize SQLite database: ${(e as Error).message}`,
|
|
55
|
-
"INIT_ERROR",
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
59
|
-
// For other DB types (Postgres/MySQL), initialize a Bun SQL client instance.
|
|
60
|
-
// This is necessary because the bare `sql` tag is for the default connection,
|
|
61
|
-
// and we need an instance with specific connection settings, using `SQL` as the constructor.
|
|
62
|
-
this.client = new SQL(config.connectionString);
|
|
63
|
-
this.logger.logDebug(
|
|
64
|
-
`Initialized Bun SQL client for: ${config.connectionString}`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private getJitter() {
|
|
70
|
-
return Math.random() * this.maxJitter;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Pool metrics are largely irrelevant for single-connection Bun SQLite
|
|
74
|
-
getPoolMetrics(): PoolMetrics {
|
|
75
|
-
return {
|
|
76
|
-
activeConnections: 0,
|
|
77
|
-
idleConnections: 0,
|
|
78
|
-
totalConnections: this.client instanceof Database ? 1 : 0,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async switchConnection(config: DBConfig) {
|
|
83
|
-
await this.close();
|
|
84
|
-
this.config = config;
|
|
85
|
-
this.preparedStatements.clear();
|
|
86
|
-
this.retryAttempts = config.retryAttempts || 3;
|
|
87
|
-
this.retryDelay = config.retryDelay || 1000;
|
|
88
|
-
this.maxJitter = config.maxJitter || 100;
|
|
89
|
-
this.initializeClient(config);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async query<T>(query: string, params: any[] = []): Promise<T[]> {
|
|
93
|
-
const start = Date.now();
|
|
94
|
-
this.logger.logQuery(query, params);
|
|
95
|
-
this.logger.logMetrics(this.getPoolMetrics());
|
|
96
|
-
|
|
97
|
-
let stmt: Statement | undefined;
|
|
98
|
-
|
|
99
|
-
// Check for Bun SQLite client
|
|
100
|
-
if (this.client instanceof Database) {
|
|
101
|
-
const stmtKey = query;
|
|
102
|
-
// Use prepared statement caching for SQLite
|
|
103
|
-
if (!this.preparedStatements.has(stmtKey)) {
|
|
104
|
-
// Use this.client (the Database instance) to prepare the statement
|
|
105
|
-
this.preparedStatements.set(stmtKey, this.client.prepare(query));
|
|
106
|
-
}
|
|
107
|
-
stmt = this.preparedStatements.get(stmtKey);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
111
|
-
try {
|
|
112
|
-
let result: T[];
|
|
113
|
-
|
|
114
|
-
if (stmt) {
|
|
115
|
-
// Bun SQLite Statement objects use .all() for fetching results
|
|
116
|
-
result = stmt.all(...params) as T[];
|
|
117
|
-
} else if (this.client) {
|
|
118
|
-
// Path for non-SQLite Bun SQL clients (Postgres/MySQL)
|
|
119
|
-
// Bun SQL clients do not expose a standard .query() method.
|
|
120
|
-
// We must use the 'unsafe' helper to execute a raw string query with positional parameters.
|
|
121
|
-
result = (await (this.client as any).unsafe(query, params)) as T[];
|
|
122
|
-
} else {
|
|
123
|
-
throw new StabilizeError(
|
|
124
|
-
"Database client is not initialized or does not support query execution.",
|
|
125
|
-
"INIT_ERROR",
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const executionTime = Date.now() - start;
|
|
130
|
-
this.logger.logQuery(query, params, executionTime);
|
|
131
|
-
return result;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
this.logger.logError(error as Error);
|
|
134
|
-
if (attempt === this.retryAttempts) {
|
|
135
|
-
throw new StabilizeError(
|
|
136
|
-
`Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`,
|
|
137
|
-
"QUERY_ERROR",
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
// Exponential backoff with jitter
|
|
141
|
-
await new Promise((resolve) =>
|
|
142
|
-
setTimeout(
|
|
143
|
-
resolve,
|
|
144
|
-
this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter(),
|
|
145
|
-
),
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
throw new StabilizeError("Query failed: no attempts made", "QUERY_ERROR");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async transaction<T>(callback: () => Promise<T>): Promise<T> {
|
|
153
|
-
// Use native Bun SQLite transaction wrapper for safer, faster transactions
|
|
154
|
-
if (this.client instanceof Database) {
|
|
155
|
-
const start = Date.now();
|
|
156
|
-
this.logger.logDebug("Starting native SQLite transaction");
|
|
157
|
-
|
|
158
|
-
// Bun's .transaction() automatically handles BEGIN/COMMIT/ROLLBACK
|
|
159
|
-
const tx = this.client.transaction(async () => {
|
|
160
|
-
return callback();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const result = await tx();
|
|
165
|
-
this.logger.logDebug(
|
|
166
|
-
`Native transaction committed in ${Date.now() - start}ms`,
|
|
167
|
-
);
|
|
168
|
-
return result;
|
|
169
|
-
} catch (error) {
|
|
170
|
-
this.logger.logError(error as Error);
|
|
171
|
-
// The transaction automatically rolls back on error
|
|
172
|
-
throw new StabilizeError(
|
|
173
|
-
`Native transaction failed: ${(error as Error).message}`,
|
|
174
|
-
"TX_ERROR",
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Fallback to manual transaction with retry logic for non-SQLite
|
|
180
|
-
const start = Date.now();
|
|
181
|
-
this.logger.logDebug("Starting manual transaction (non-SQLite)");
|
|
182
|
-
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
183
|
-
try {
|
|
184
|
-
// Use this.query, which now correctly handles execution via .unsafe() for non-SQLite
|
|
185
|
-
await this.query("BEGIN", []);
|
|
186
|
-
const result = await callback();
|
|
187
|
-
await this.query("COMMIT", []);
|
|
188
|
-
this.logger.logDebug(
|
|
189
|
-
`Manual transaction committed in ${Date.now() - start}ms`,
|
|
190
|
-
);
|
|
191
|
-
return result;
|
|
192
|
-
} catch (error) {
|
|
193
|
-
// Attempt rollback, but ignore errors if rollback fails
|
|
194
|
-
await this.query("ROLLBACK", []).catch(() => {
|
|
195
|
-
this.logger.logDebug("Rollback failed. Connection may be invalid.");
|
|
196
|
-
});
|
|
197
|
-
this.logger.logError(error as Error);
|
|
198
|
-
if (attempt === this.retryAttempts) {
|
|
199
|
-
throw new StabilizeError(
|
|
200
|
-
`Manual transaction failed after ${this.retryAttempts} attempts: ${(error as Error).message}`,
|
|
201
|
-
"TX_ERROR",
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
// Exponential backoff with jitter
|
|
205
|
-
await new Promise((resolve) =>
|
|
206
|
-
setTimeout(
|
|
207
|
-
resolve,
|
|
208
|
-
this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter(),
|
|
209
|
-
),
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
throw new StabilizeError(
|
|
214
|
-
"Transaction failed: no attempts made",
|
|
215
|
-
"TX_ERROR",
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async savepoint<T>(name: string, callback: () => Promise<T>): Promise<T> {
|
|
220
|
-
const start = Date.now();
|
|
221
|
-
this.logger.logDebug(`Starting savepoint ${name}`);
|
|
222
|
-
await this.query(`SAVEPOINT ${name}`, []);
|
|
223
|
-
try {
|
|
224
|
-
const result = await callback();
|
|
225
|
-
await this.query(`RELEASE SAVEPOINT ${name}`, []);
|
|
226
|
-
this.logger.logDebug(
|
|
227
|
-
`Savepoint ${name} released in ${Date.now() - start}ms`,
|
|
228
|
-
);
|
|
229
|
-
return result;
|
|
230
|
-
} catch (error) {
|
|
231
|
-
// Attempt rollback, but ignore errors if rollback fails
|
|
232
|
-
await this.query(`ROLLBACK TO SAVEPOINT ${name}`, []).catch(() => {
|
|
233
|
-
this.logger.logDebug(
|
|
234
|
-
`Rollback to savepoint ${name} failed. Connection may be invalid.`,
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
this.logger.logError(error as Error);
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async close() {
|
|
243
|
-
this.preparedStatements.clear();
|
|
244
|
-
// Check if the client is a Bun Database instance before closing (Bun SQLite close is sync)
|
|
245
|
-
if (this.client instanceof Database) {
|
|
246
|
-
this.client.close();
|
|
247
|
-
this.client = null;
|
|
248
|
-
} else if (
|
|
249
|
-
this.client &&
|
|
250
|
-
typeof (this.client as any).close === "function"
|
|
251
|
-
) {
|
|
252
|
-
// Assume external/Bun SQL driver has an async close method
|
|
253
|
-
await (this.client as any).close();
|
|
254
|
-
this.client = null;
|
|
255
|
-
}
|
|
256
|
-
this.logger.logInfo("Database connection closed");
|
|
257
|
-
}
|
|
258
|
-
}
|
package/src/decorators.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import "reflect-metadata";
|
|
2
|
-
import { RelationType } from "./types";
|
|
3
|
-
|
|
4
|
-
export const ModelKey = Symbol("model");
|
|
5
|
-
export const ColumnKey = Symbol("column");
|
|
6
|
-
export const ValidatorKey = Symbol("validator");
|
|
7
|
-
export const RelationKey = Symbol("relation");
|
|
8
|
-
export const SoftDeleteKey = Symbol("softDelete");
|
|
9
|
-
|
|
10
|
-
export function Model(tableName: string) {
|
|
11
|
-
return function (constructor: Function) {
|
|
12
|
-
Reflect.defineMetadata(ModelKey, tableName, constructor);
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Column(name: string, type: string) {
|
|
17
|
-
return function (target: any, propertyKey: string) {
|
|
18
|
-
const columns = Reflect.getMetadata(ColumnKey, target) || {};
|
|
19
|
-
columns[propertyKey] = { name, type };
|
|
20
|
-
Reflect.defineMetadata(ColumnKey, columns, target);
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function Required() {
|
|
25
|
-
return function (target: any, propertyKey: string) {
|
|
26
|
-
const validators = Reflect.getMetadata(ValidatorKey, target) || {};
|
|
27
|
-
validators[propertyKey] = [...(validators[propertyKey] || []), "required"];
|
|
28
|
-
Reflect.defineMetadata(ValidatorKey, validators, target);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function Unique() {
|
|
33
|
-
return function (target: any, propertyKey: string) {
|
|
34
|
-
const validators = Reflect.getMetadata(ValidatorKey, target) || {};
|
|
35
|
-
validators[propertyKey] = [...(validators[propertyKey] || []), "unique"];
|
|
36
|
-
Reflect.defineMetadata(ValidatorKey, validators, target);
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function SoftDelete() {
|
|
41
|
-
return function (target: any, propertyKey: string) {
|
|
42
|
-
Reflect.defineMetadata(SoftDeleteKey, propertyKey, target);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function OneToOne(model: () => any, foreignKey: string) {
|
|
47
|
-
return function (target: any, propertyKey: string) {
|
|
48
|
-
const relations = Reflect.getMetadata(RelationKey, target) || {};
|
|
49
|
-
relations[propertyKey] = {
|
|
50
|
-
type: RelationType.OneToOne,
|
|
51
|
-
targetModel: model,
|
|
52
|
-
foreignKey,
|
|
53
|
-
};
|
|
54
|
-
Reflect.defineMetadata(RelationKey, relations, target);
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function ManyToOne(model: () => any, foreignKey: string) {
|
|
59
|
-
return function (target: any, propertyKey: string) {
|
|
60
|
-
const relations = Reflect.getMetadata(RelationKey, target) || {};
|
|
61
|
-
relations[propertyKey] = {
|
|
62
|
-
type: RelationType.ManyToOne,
|
|
63
|
-
targetModel: model,
|
|
64
|
-
foreignKey,
|
|
65
|
-
};
|
|
66
|
-
Reflect.defineMetadata(RelationKey, relations, target);
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function OneToMany(model: () => any, inverseKey: string) {
|
|
71
|
-
return function (target: any, propertyKey: string) {
|
|
72
|
-
const relations = Reflect.getMetadata(RelationKey, target) || {};
|
|
73
|
-
relations[propertyKey] = {
|
|
74
|
-
type: RelationType.OneToMany,
|
|
75
|
-
targetModel: model,
|
|
76
|
-
inverseKey,
|
|
77
|
-
};
|
|
78
|
-
Reflect.defineMetadata(RelationKey, relations, target);
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function ManyToMany(
|
|
83
|
-
model: () => any,
|
|
84
|
-
joinTable: string,
|
|
85
|
-
foreignKey: string,
|
|
86
|
-
inverseKey: string,
|
|
87
|
-
) {
|
|
88
|
-
return function (target: any, propertyKey: string) {
|
|
89
|
-
const relations = Reflect.getMetadata(RelationKey, target) || {};
|
|
90
|
-
relations[propertyKey] = {
|
|
91
|
-
type: RelationType.ManyToMany,
|
|
92
|
-
targetModel: model,
|
|
93
|
-
joinTable,
|
|
94
|
-
foreignKey,
|
|
95
|
-
inverseKey,
|
|
96
|
-
};
|
|
97
|
-
Reflect.defineMetadata(RelationKey, relations, target);
|
|
98
|
-
};
|
|
99
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { Cache } from "./cache";
|
|
2
|
-
import { DBClient } from "./client";
|
|
3
|
-
import { type Logger, ConsoleLogger } from "./logger";
|
|
4
|
-
import { QueryBuilder } from "./query-builder";
|
|
5
|
-
import { Repository } from "./repository";
|
|
6
|
-
import { runMigrations, generateMigration, type Migration } from "./migrations";
|
|
7
|
-
import {
|
|
8
|
-
Model,
|
|
9
|
-
Column,
|
|
10
|
-
Required,
|
|
11
|
-
Unique,
|
|
12
|
-
SoftDelete,
|
|
13
|
-
OneToOne,
|
|
14
|
-
ManyToOne,
|
|
15
|
-
OneToMany,
|
|
16
|
-
ManyToMany,
|
|
17
|
-
ModelKey,
|
|
18
|
-
ColumnKey,
|
|
19
|
-
ValidatorKey,
|
|
20
|
-
RelationKey,
|
|
21
|
-
SoftDeleteKey,
|
|
22
|
-
} from "./decorators";
|
|
23
|
-
import {
|
|
24
|
-
type DBConfig,
|
|
25
|
-
type CacheConfig,
|
|
26
|
-
type LoggerConfig,
|
|
27
|
-
DBType,
|
|
28
|
-
StabilizeError,
|
|
29
|
-
type PoolMetrics,
|
|
30
|
-
type QueryHint,
|
|
31
|
-
RelationType,
|
|
32
|
-
type CacheStats,
|
|
33
|
-
LogLevel,
|
|
34
|
-
} from "./types";
|
|
35
|
-
|
|
36
|
-
export class Stabilize {
|
|
37
|
-
private client: DBClient;
|
|
38
|
-
private cache: Cache | null;
|
|
39
|
-
private logger: Logger;
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
config: DBConfig,
|
|
43
|
-
cacheConfig: CacheConfig = { enabled: false, ttl: 60 },
|
|
44
|
-
loggerConfig: LoggerConfig = {},
|
|
45
|
-
) {
|
|
46
|
-
this.logger = new ConsoleLogger(loggerConfig);
|
|
47
|
-
this.client = new DBClient(config, this.logger);
|
|
48
|
-
this.cache = cacheConfig.enabled
|
|
49
|
-
? new Cache(cacheConfig, this.logger)
|
|
50
|
-
: null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
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);
|
|
76
|
-
}
|
|
77
|
-
|
|
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);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
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();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async close() {
|
|
97
|
-
await this.client.close();
|
|
98
|
-
if (this.cache) await this.cache.disconnect();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export {
|
|
103
|
-
// Types
|
|
104
|
-
DBType,
|
|
105
|
-
LogLevel,
|
|
106
|
-
RelationType,
|
|
107
|
-
StabilizeError,
|
|
108
|
-
// Decorators
|
|
109
|
-
Model,
|
|
110
|
-
Column,
|
|
111
|
-
Required,
|
|
112
|
-
Unique,
|
|
113
|
-
SoftDelete,
|
|
114
|
-
OneToOne,
|
|
115
|
-
ManyToOne,
|
|
116
|
-
OneToMany,
|
|
117
|
-
ManyToMany,
|
|
118
|
-
ModelKey,
|
|
119
|
-
ColumnKey,
|
|
120
|
-
ValidatorKey,
|
|
121
|
-
RelationKey,
|
|
122
|
-
SoftDeleteKey,
|
|
123
|
-
// Classes
|
|
124
|
-
Cache,
|
|
125
|
-
DBClient,
|
|
126
|
-
QueryBuilder,
|
|
127
|
-
Repository,
|
|
128
|
-
ConsoleLogger,
|
|
129
|
-
// Migrations
|
|
130
|
-
runMigrations,
|
|
131
|
-
generateMigration,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export type { Migration };
|
|
135
|
-
export type {
|
|
136
|
-
DBConfig,
|
|
137
|
-
CacheConfig,
|
|
138
|
-
LoggerConfig,
|
|
139
|
-
QueryHint,
|
|
140
|
-
PoolMetrics,
|
|
141
|
-
CacheStats,
|
|
142
|
-
Logger,
|
|
143
|
-
};
|
package/src/logger.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// src/logger.ts
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import {
|
|
5
|
-
LogLevel,
|
|
6
|
-
type LoggerConfig,
|
|
7
|
-
type PoolMetrics,
|
|
8
|
-
StabilizeError,
|
|
9
|
-
} from "./types";
|
|
10
|
-
|
|
11
|
-
export interface Logger {
|
|
12
|
-
logQuery(query: string, params: any[], executionTime?: number): void;
|
|
13
|
-
logError(error: Error): void;
|
|
14
|
-
logMetrics(metrics: PoolMetrics): void;
|
|
15
|
-
logInfo(message: string): void;
|
|
16
|
-
logDebug(message: string): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
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;
|
|
25
|
-
|
|
26
|
-
constructor(config: LoggerConfig = {}) {
|
|
27
|
-
this.level = config.level || LogLevel.INFO;
|
|
28
|
-
this.filePath = config.filePath || null;
|
|
29
|
-
this.maxFileSize = config.maxFileSize || 1 * 1024 * 1024; // 1MB
|
|
30
|
-
this.maxFiles = config.maxFiles || 3;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
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);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private async rotateLogFile() {
|
|
44
|
-
if (!this.filePath) return;
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
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
|
-
}
|
|
55
|
-
}
|
|
56
|
-
await fs.writeFile(this.filePath, "");
|
|
57
|
-
this.currentFileSize = 0;
|
|
58
|
-
}
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error("Log rotation failed:", error);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private async writeToFile(message: string) {
|
|
65
|
-
if (!this.filePath) return;
|
|
66
|
-
|
|
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);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
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);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
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
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
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
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
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
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
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
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
package/src/migrations.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { DBClient } from "./client";
|
|
2
|
-
import { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey } from "./decorators";
|
|
3
|
-
import { type DBConfig, type Migration, StabilizeError } from "./types";
|
|
4
|
-
|
|
5
|
-
type ColumnData = { name: string; type: string };
|
|
6
|
-
type ColumnMetadata = Record<string, ColumnData>;
|
|
7
|
-
type ValidatorMetadata = Record<string, string[]>;
|
|
8
|
-
|
|
9
|
-
export async function generateMigration(
|
|
10
|
-
model: new (...args: any[]) => any,
|
|
11
|
-
name: string,
|
|
12
|
-
): Promise<Migration> {
|
|
13
|
-
const tableName = Reflect.getMetadata(ModelKey, model);
|
|
14
|
-
if (!tableName)
|
|
15
|
-
throw new StabilizeError(
|
|
16
|
-
"Model not decorated with @Model",
|
|
17
|
-
"MIGRATION_ERROR",
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const columns: ColumnMetadata =
|
|
21
|
-
Reflect.getMetadata(ColumnKey, model.prototype) || {};
|
|
22
|
-
const validators: ValidatorMetadata =
|
|
23
|
-
Reflect.getMetadata(ValidatorKey, model.prototype) || {};
|
|
24
|
-
const softDeleteField = Reflect.getMetadata(SoftDeleteKey, model.prototype);
|
|
25
|
-
|
|
26
|
-
const columnDefs = Object.entries(columns).map(([key, col]) => {
|
|
27
|
-
let def = `${col.name} ${col.type}`;
|
|
28
|
-
if (col.name === "id") def += " PRIMARY KEY AUTOINCREMENT";
|
|
29
|
-
if (validators[key]?.includes("required")) def += " NOT NULL";
|
|
30
|
-
if (validators[key]?.includes("unique")) def += " UNIQUE";
|
|
31
|
-
return def;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (softDeleteField && columns[softDeleteField]) {
|
|
35
|
-
columnDefs.push(
|
|
36
|
-
`${columns[softDeleteField].name} ${columns[softDeleteField].type}`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const up = [
|
|
41
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`,
|
|
42
|
-
];
|
|
43
|
-
const down = [`DROP TABLE IF EXISTS ${tableName}`];
|
|
44
|
-
|
|
45
|
-
return { up, down };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function runMigrations(config: DBConfig, migrations: Migration[]) {
|
|
49
|
-
const client = new DBClient(config);
|
|
50
|
-
try {
|
|
51
|
-
await client.query(`
|
|
52
|
-
CREATE TABLE IF NOT EXISTS migrations (
|
|
53
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
-
name TEXT NOT NULL,
|
|
55
|
-
applied_at TEXT NOT NULL
|
|
56
|
-
)
|
|
57
|
-
`);
|
|
58
|
-
|
|
59
|
-
for (const [index, migration] of migrations.entries()) {
|
|
60
|
-
const name = `migration_${index}_${new Date().toISOString().replace(/[-:T.]/g, "")}`;
|
|
61
|
-
const applied = await client.query<{ id: number }>(
|
|
62
|
-
`SELECT id FROM migrations WHERE name = ?`,
|
|
63
|
-
[name],
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
if (applied.length === 0) {
|
|
67
|
-
for (const query of migration.up) {
|
|
68
|
-
await client.query(query, []);
|
|
69
|
-
}
|
|
70
|
-
await client.query(
|
|
71
|
-
`INSERT INTO migrations (name, applied_at) VALUES (?, ?)`,
|
|
72
|
-
[name, new Date().toISOString()],
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
} finally {
|
|
77
|
-
await client.close();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type { Migration };
|