stabilize-orm 1.1.1 → 1.1.3
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/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +23 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- package/.github/workflows/ci-cd.yml +4 -55
- package/CHANGELOG.md +23 -0
- package/CODE_OF_CONDUCT.md +87 -0
- package/CONTRIBUTING.md +48 -0
- package/FUNDING.md +14 -0
- package/README.md +273 -32
- package/SECURITY.md +35 -0
- package/SUPPORT.md +18 -0
- package/bun.lock +6 -0
- package/cli/stabilize-cli.ts +290 -319
- package/client.ts +5 -30
- package/migrations.ts +76 -11
- package/package.json +5 -3
package/client.ts
CHANGED
|
@@ -4,12 +4,10 @@ import {
|
|
|
4
4
|
type DBConfig,
|
|
5
5
|
StabilizeError,
|
|
6
6
|
type PoolMetrics,
|
|
7
|
-
DBType,
|
|
8
|
-
type LoggerConfig, // Kept, but not used in logic
|
|
7
|
+
DBType,
|
|
9
8
|
} from "./types";
|
|
10
9
|
import { type Logger, ConsoleLogger } from "./logger";
|
|
11
10
|
|
|
12
|
-
// Helper to determine if we are in Bun SQLite mode based on config
|
|
13
11
|
function isSQLiteConfig(config: DBConfig): boolean {
|
|
14
12
|
return (
|
|
15
13
|
config.type === DBType.SQLite || config.connectionString.includes("sqlite")
|
|
@@ -17,14 +15,12 @@ function isSQLiteConfig(config: DBConfig): boolean {
|
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
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
18
|
private client: Database | any | null = null;
|
|
23
19
|
private logger: Logger;
|
|
24
20
|
private retryAttempts: number;
|
|
25
21
|
private retryDelay: number;
|
|
26
22
|
private maxJitter: number;
|
|
27
|
-
|
|
23
|
+
|
|
28
24
|
private preparedStatements: Map<string, Statement> = new Map();
|
|
29
25
|
private config: DBConfig;
|
|
30
26
|
|
|
@@ -38,10 +34,8 @@ export class DBClient {
|
|
|
38
34
|
this.initializeClient(config);
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
// Initializes the connection based on config
|
|
42
37
|
private initializeClient(config: DBConfig) {
|
|
43
38
|
if (isSQLiteConfig(config)) {
|
|
44
|
-
// Use Bun's native SQLite client, which is synchronous to construct
|
|
45
39
|
try {
|
|
46
40
|
// new Database(path, { create: true }) is the standard Bun method
|
|
47
41
|
this.client = new Database(config.connectionString, { create: true });
|
|
@@ -56,9 +50,6 @@ export class DBClient {
|
|
|
56
50
|
);
|
|
57
51
|
}
|
|
58
52
|
} 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
53
|
this.client = new SQL(config.connectionString);
|
|
63
54
|
this.logger.logDebug(
|
|
64
55
|
`Initialized Bun SQL client for: ${config.connectionString}`,
|
|
@@ -70,7 +61,6 @@ export class DBClient {
|
|
|
70
61
|
return Math.random() * this.maxJitter;
|
|
71
62
|
}
|
|
72
63
|
|
|
73
|
-
// Pool metrics are largely irrelevant for single-connection Bun SQLite
|
|
74
64
|
getPoolMetrics(): PoolMetrics {
|
|
75
65
|
return {
|
|
76
66
|
activeConnections: 0,
|
|
@@ -112,13 +102,9 @@ export class DBClient {
|
|
|
112
102
|
let result: T[];
|
|
113
103
|
|
|
114
104
|
if (stmt) {
|
|
115
|
-
// Bun SQLite Statement objects use .all() for fetching results
|
|
116
105
|
result = stmt.all(...params) as T[];
|
|
117
106
|
} else if (this.client) {
|
|
118
|
-
|
|
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[];
|
|
107
|
+
result = (await (this.client as any).unsafe(query, params)) as T[];
|
|
122
108
|
} else {
|
|
123
109
|
throw new StabilizeError(
|
|
124
110
|
"Database client is not initialized or does not support query execution.",
|
|
@@ -137,7 +123,6 @@ export class DBClient {
|
|
|
137
123
|
"QUERY_ERROR",
|
|
138
124
|
);
|
|
139
125
|
}
|
|
140
|
-
// Exponential backoff with jitter
|
|
141
126
|
await new Promise((resolve) =>
|
|
142
127
|
setTimeout(
|
|
143
128
|
resolve,
|
|
@@ -150,12 +135,10 @@ export class DBClient {
|
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
async transaction<T>(callback: () => Promise<T>): Promise<T> {
|
|
153
|
-
// Use native Bun SQLite transaction wrapper for safer, faster transactions
|
|
154
138
|
if (this.client instanceof Database) {
|
|
155
139
|
const start = Date.now();
|
|
156
140
|
this.logger.logDebug("Starting native SQLite transaction");
|
|
157
141
|
|
|
158
|
-
// Bun's .transaction() automatically handles BEGIN/COMMIT/ROLLBACK
|
|
159
142
|
const tx = this.client.transaction(async () => {
|
|
160
143
|
return callback();
|
|
161
144
|
});
|
|
@@ -168,7 +151,6 @@ export class DBClient {
|
|
|
168
151
|
return result;
|
|
169
152
|
} catch (error) {
|
|
170
153
|
this.logger.logError(error as Error);
|
|
171
|
-
// The transaction automatically rolls back on error
|
|
172
154
|
throw new StabilizeError(
|
|
173
155
|
`Native transaction failed: ${(error as Error).message}`,
|
|
174
156
|
"TX_ERROR",
|
|
@@ -176,13 +158,11 @@ export class DBClient {
|
|
|
176
158
|
}
|
|
177
159
|
}
|
|
178
160
|
|
|
179
|
-
// Fallback to manual transaction with retry logic for non-SQLite
|
|
180
161
|
const start = Date.now();
|
|
181
162
|
this.logger.logDebug("Starting manual transaction (non-SQLite)");
|
|
182
163
|
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
183
164
|
try {
|
|
184
|
-
|
|
185
|
-
await this.query("BEGIN", []);
|
|
165
|
+
await this.query("BEGIN", []);
|
|
186
166
|
const result = await callback();
|
|
187
167
|
await this.query("COMMIT", []);
|
|
188
168
|
this.logger.logDebug(
|
|
@@ -190,7 +170,6 @@ export class DBClient {
|
|
|
190
170
|
);
|
|
191
171
|
return result;
|
|
192
172
|
} catch (error) {
|
|
193
|
-
// Attempt rollback, but ignore errors if rollback fails
|
|
194
173
|
await this.query("ROLLBACK", []).catch(() => {
|
|
195
174
|
this.logger.logDebug("Rollback failed. Connection may be invalid.");
|
|
196
175
|
});
|
|
@@ -201,7 +180,6 @@ export class DBClient {
|
|
|
201
180
|
"TX_ERROR",
|
|
202
181
|
);
|
|
203
182
|
}
|
|
204
|
-
// Exponential backoff with jitter
|
|
205
183
|
await new Promise((resolve) =>
|
|
206
184
|
setTimeout(
|
|
207
185
|
resolve,
|
|
@@ -228,7 +206,6 @@ export class DBClient {
|
|
|
228
206
|
);
|
|
229
207
|
return result;
|
|
230
208
|
} catch (error) {
|
|
231
|
-
// Attempt rollback, but ignore errors if rollback fails
|
|
232
209
|
await this.query(`ROLLBACK TO SAVEPOINT ${name}`, []).catch(() => {
|
|
233
210
|
this.logger.logDebug(
|
|
234
211
|
`Rollback to savepoint ${name} failed. Connection may be invalid.`,
|
|
@@ -241,15 +218,13 @@ export class DBClient {
|
|
|
241
218
|
|
|
242
219
|
async close() {
|
|
243
220
|
this.preparedStatements.clear();
|
|
244
|
-
|
|
245
|
-
if (this.client instanceof Database) {
|
|
221
|
+
if (this.client instanceof Database) {
|
|
246
222
|
this.client.close();
|
|
247
223
|
this.client = null;
|
|
248
224
|
} else if (
|
|
249
225
|
this.client &&
|
|
250
226
|
typeof (this.client as any).close === "function"
|
|
251
227
|
) {
|
|
252
|
-
// Assume external/Bun SQL driver has an async close method
|
|
253
228
|
await (this.client as any).close();
|
|
254
229
|
this.client = null;
|
|
255
230
|
}
|
package/migrations.ts
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import { DBClient } from "./client";
|
|
2
2
|
import { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey } from "./decorators";
|
|
3
|
-
import { type DBConfig, type Migration, StabilizeError } from "./types";
|
|
3
|
+
import { type DBConfig, type Migration, StabilizeError, DBType } from "./types";
|
|
4
4
|
|
|
5
5
|
type ColumnData = { name: string; type: string };
|
|
6
6
|
type ColumnMetadata = Record<string, ColumnData>;
|
|
7
7
|
type ValidatorMetadata = Record<string, string[]>;
|
|
8
8
|
|
|
9
|
+
// Helper to get SQL type for auto-increment PK
|
|
10
|
+
function getAutoIncrementPK(dbType: DBType) {
|
|
11
|
+
switch (dbType) {
|
|
12
|
+
case DBType.Postgres:
|
|
13
|
+
return "SERIAL PRIMARY KEY";
|
|
14
|
+
case DBType.MySQL:
|
|
15
|
+
return "INT AUTO_INCREMENT PRIMARY KEY";
|
|
16
|
+
case DBType.SQLite:
|
|
17
|
+
return "INTEGER PRIMARY KEY AUTOINCREMENT";
|
|
18
|
+
default: // Default to Postgres
|
|
19
|
+
return "SERIAL PRIMARY KEY";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to get SQL type for timestamps
|
|
24
|
+
function getTimestampType(dbType: DBType) {
|
|
25
|
+
switch (dbType) {
|
|
26
|
+
case DBType.Postgres:
|
|
27
|
+
return "TIMESTAMP";
|
|
28
|
+
case DBType.MySQL:
|
|
29
|
+
return "DATETIME";
|
|
30
|
+
case DBType.SQLite:
|
|
31
|
+
return "TEXT";
|
|
32
|
+
default: // Default to Postgres
|
|
33
|
+
return "TIMESTAMP";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
export async function generateMigration(
|
|
10
38
|
model: new (...args: any[]) => any,
|
|
11
39
|
name: string,
|
|
40
|
+
dbType: DBType = DBType.Postgres, // default to Postgres
|
|
12
41
|
): Promise<Migration> {
|
|
13
42
|
const tableName = Reflect.getMetadata(ModelKey, model);
|
|
14
43
|
if (!tableName)
|
|
@@ -24,10 +53,19 @@ export async function generateMigration(
|
|
|
24
53
|
const softDeleteField = Reflect.getMetadata(SoftDeleteKey, model.prototype);
|
|
25
54
|
|
|
26
55
|
const columnDefs = Object.entries(columns).map(([key, col]) => {
|
|
27
|
-
let def
|
|
28
|
-
if (col.name === "id")
|
|
56
|
+
let def: string;
|
|
57
|
+
if (col.name === "id") {
|
|
58
|
+
// Use correct PK syntax for each DB
|
|
59
|
+
def = getAutoIncrementPK(dbType);
|
|
60
|
+
return def;
|
|
61
|
+
}
|
|
62
|
+
def = `${col.name} ${col.type}`;
|
|
29
63
|
if (validators[key]?.includes("required")) def += " NOT NULL";
|
|
30
64
|
if (validators[key]?.includes("unique")) def += " UNIQUE";
|
|
65
|
+
// Handle timestamps
|
|
66
|
+
if (["createdAt", "updatedAt"].includes(col.name)) {
|
|
67
|
+
def = `${col.name} ${getTimestampType(dbType)}`;
|
|
68
|
+
}
|
|
31
69
|
return def;
|
|
32
70
|
});
|
|
33
71
|
|
|
@@ -45,16 +83,43 @@ export async function generateMigration(
|
|
|
45
83
|
return { up, down };
|
|
46
84
|
}
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
CREATE TABLE IF NOT EXISTS migrations (
|
|
86
|
+
// Create migrations table with correct types for each DB
|
|
87
|
+
function getMigrationsTableSQL(dbType: DBType) {
|
|
88
|
+
switch (dbType) {
|
|
89
|
+
case DBType.Postgres:
|
|
90
|
+
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
91
|
+
id SERIAL PRIMARY KEY,
|
|
92
|
+
name TEXT NOT NULL,
|
|
93
|
+
applied_at TIMESTAMP NOT NULL
|
|
94
|
+
)`;
|
|
95
|
+
case DBType.MySQL:
|
|
96
|
+
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
97
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
98
|
+
name VARCHAR(255) NOT NULL,
|
|
99
|
+
applied_at DATETIME NOT NULL
|
|
100
|
+
)`;
|
|
101
|
+
case DBType.SQLite:
|
|
102
|
+
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
53
103
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
104
|
name TEXT NOT NULL,
|
|
55
105
|
applied_at TEXT NOT NULL
|
|
56
|
-
)
|
|
57
|
-
|
|
106
|
+
)`;
|
|
107
|
+
default: // Default to Postgres
|
|
108
|
+
return `CREATE TABLE IF NOT EXISTS migrations (
|
|
109
|
+
id SERIAL PRIMARY KEY,
|
|
110
|
+
name TEXT NOT NULL,
|
|
111
|
+
applied_at TIMESTAMP NOT NULL
|
|
112
|
+
)`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runMigrations(config: DBConfig, migrations: Migration[]) {
|
|
117
|
+
const client = new DBClient(config);
|
|
118
|
+
try {
|
|
119
|
+
// Detect DB type from config
|
|
120
|
+
const dbType = config.type ?? DBType.Postgres;
|
|
121
|
+
|
|
122
|
+
await client.query(getMigrationsTableSQL(dbType));
|
|
58
123
|
|
|
59
124
|
for (const [index, migration] of migrations.entries()) {
|
|
60
125
|
const name = `migration_${index}_${new Date().toISOString().replace(/[-:T.]/g, "")}`;
|
|
@@ -78,4 +143,4 @@ export async function runMigrations(config: DBConfig, migrations: Migration[]) {
|
|
|
78
143
|
}
|
|
79
144
|
}
|
|
80
145
|
|
|
81
|
-
export type { Migration };
|
|
146
|
+
export type { Migration };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stabilize-orm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,14 +25,16 @@
|
|
|
25
25
|
"typescript",
|
|
26
26
|
"database"
|
|
27
27
|
],
|
|
28
|
-
"author": "
|
|
28
|
+
"author": "ElectronSz <lwazicd@icloud.com>",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@types/uuid": "^11.0.0",
|
|
31
32
|
"commander": "^12.1.0",
|
|
32
33
|
"figlet": "^1.9.3",
|
|
33
34
|
"glob": "^11.0.0",
|
|
34
35
|
"ioredis": "^5.4.1",
|
|
35
|
-
"reflect-metadata": "^0.2.2"
|
|
36
|
+
"reflect-metadata": "^0.2.2",
|
|
37
|
+
"uuid": "^13.0.0"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@typescript-eslint/eslint-plugin": "^8.7.0",
|