stabilize-orm 1.2.0 → 1.3.0
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/CHANGELOG.md +33 -5
- package/README.md +165 -129
- package/client.ts +7 -82
- package/hooks.ts +65 -22
- package/index.ts +7 -39
- package/migrations.ts +23 -38
- package/model.ts +115 -0
- package/package.json +3 -9
- package/repository.ts +56 -66
- package/decorators.ts +0 -170
package/client.ts
CHANGED
|
@@ -14,26 +14,18 @@ import {
|
|
|
14
14
|
} from "./types";
|
|
15
15
|
import { type Logger, ConsoleLogger } from "./logger";
|
|
16
16
|
|
|
17
|
-
/** @internal Checks if the config is for SQLite. */
|
|
18
17
|
function isSQLiteConfig(config: DBConfig): boolean {
|
|
19
18
|
return config.type === DBType.SQLite;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
/** @internal Checks if the config is for MySQL. */
|
|
23
21
|
function isMySQLConfig(config: DBConfig): boolean {
|
|
24
22
|
return config.type === DBType.MySQL;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
/** @internal A type guard to reliably identify a mysql2 Pool object. */
|
|
28
25
|
function isMySQLPool(client: any): client is mysql.Pool {
|
|
29
26
|
return typeof client.getConnection === 'function';
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
/**
|
|
33
|
-
* A unified database client that provides a consistent interface for
|
|
34
|
-
* PostgreSQL, MySQL, and SQLite databases. It handles connection pooling,
|
|
35
|
-
* query execution with retries, and transactions.
|
|
36
|
-
*/
|
|
37
29
|
export class DBClient {
|
|
38
30
|
private client!: Database | Pool | mysql.Pool | PoolClient | mysql.PoolConnection;
|
|
39
31
|
private logger: Logger;
|
|
@@ -45,12 +37,6 @@ export class DBClient {
|
|
|
45
37
|
private preparedStatements: Map<string, Statement> = new Map();
|
|
46
38
|
public readonly isTransactionClient: boolean = false;
|
|
47
39
|
|
|
48
|
-
/**
|
|
49
|
-
* Creates an instance of DBClient.
|
|
50
|
-
* @param config The database configuration object.
|
|
51
|
-
* @param logger A logger instance for logging messages.
|
|
52
|
-
* @param existingClient An optional existing connection, used internally for transactions.
|
|
53
|
-
*/
|
|
54
40
|
constructor(
|
|
55
41
|
config: DBConfig,
|
|
56
42
|
logger: Logger = new ConsoleLogger(),
|
|
@@ -70,11 +56,6 @@ export class DBClient {
|
|
|
70
56
|
}
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
/**
|
|
74
|
-
* @internal
|
|
75
|
-
* Initializes the database client based on the provided configuration.
|
|
76
|
-
* @param config The database configuration.
|
|
77
|
-
*/
|
|
78
59
|
private initializeClient(config: DBConfig) {
|
|
79
60
|
if (isSQLiteConfig(config)) {
|
|
80
61
|
this.client = new Database(config.connectionString, { create: true });
|
|
@@ -88,22 +69,8 @@ export class DBClient {
|
|
|
88
69
|
}
|
|
89
70
|
}
|
|
90
71
|
|
|
91
|
-
/** @internal Gets a random jitter value to add to retry delays. */
|
|
92
72
|
private getJitter = () => Math.random() * this.maxJitter;
|
|
93
73
|
|
|
94
|
-
/**
|
|
95
|
-
* Executes a SQL query with parameters and returns the result.
|
|
96
|
-
* Automatically handles placeholder conversion for different databases and includes retry logic.
|
|
97
|
-
* @template T The expected type of the result rows.
|
|
98
|
-
* @param query The SQL query string with `?` as placeholders.
|
|
99
|
-
* @param params An array of parameters to bind to the query.
|
|
100
|
-
* @returns A promise that resolves to an array of results.
|
|
101
|
-
* @example
|
|
102
|
-
* ```
|
|
103
|
-
* const users = await dbClient.query('SELECT * FROM users WHERE status = ?', ['active']);
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
|
|
107
74
|
async query<T>(query: string, params: any[] = []): Promise<T[]> {
|
|
108
75
|
const start = Date.now();
|
|
109
76
|
|
|
@@ -111,21 +78,19 @@ export class DBClient {
|
|
|
111
78
|
try {
|
|
112
79
|
let result: any;
|
|
113
80
|
|
|
114
|
-
// Log the query before execution
|
|
115
81
|
this.logger.logQuery(query, params);
|
|
116
82
|
|
|
117
|
-
if (this.client instanceof Database) {
|
|
83
|
+
if (this.client instanceof Database) {
|
|
118
84
|
let stmt = this.preparedStatements.get(query);
|
|
119
85
|
if (!stmt) {
|
|
120
86
|
stmt = this.client.prepare(query);
|
|
121
87
|
this.preparedStatements.set(query, stmt);
|
|
122
88
|
}
|
|
123
89
|
result = stmt.all(...params);
|
|
124
|
-
} else if (this.config.type === DBType.MySQL && isMySQLPool(this.client)) {
|
|
90
|
+
} else if (this.config.type === DBType.MySQL && isMySQLPool(this.client)) {
|
|
125
91
|
const [rows] = await (this.client as mysql.Pool).query(query, params);
|
|
126
92
|
result = rows;
|
|
127
|
-
} else if (this.config.type === DBType.Postgres ) {
|
|
128
|
-
|
|
93
|
+
} else if (this.config.type === DBType.Postgres ) {
|
|
129
94
|
let paramIndex = 0;
|
|
130
95
|
const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
|
|
131
96
|
const pgResult = await (this.client as Pool).query(pgQuery, params);
|
|
@@ -138,8 +103,6 @@ export class DBClient {
|
|
|
138
103
|
this.logger.logQuery(query, params, executionTime);
|
|
139
104
|
return Array.isArray(result) ? result as T[] : [];
|
|
140
105
|
} catch (error) {
|
|
141
|
-
console.log("error: ", error);
|
|
142
|
-
|
|
143
106
|
this.logger.logError(error as Error);
|
|
144
107
|
if (attempt === this.retryAttempts) {
|
|
145
108
|
throw new StabilizeError(`Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`, "QUERY_ERROR");
|
|
@@ -147,24 +110,9 @@ export class DBClient {
|
|
|
147
110
|
await new Promise(res => setTimeout(res, this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter()));
|
|
148
111
|
}
|
|
149
112
|
}
|
|
150
|
-
// This line should theoretically be unreachable if retryAttempts >= 1
|
|
151
113
|
throw new StabilizeError("Query failed: maximum retries reached without success", "QUERY_ERROR");
|
|
152
114
|
}
|
|
153
115
|
|
|
154
|
-
/**
|
|
155
|
-
* Executes a series of database operations within a single atomic transaction.
|
|
156
|
-
* If any operation in the callback fails, the entire transaction is rolled back.
|
|
157
|
-
* @template T The return type of the callback function.
|
|
158
|
-
* @param callback An async function that receives a transactional `DBClient` instance.
|
|
159
|
-
* @returns A promise that resolves with the result of the callback.
|
|
160
|
-
* @example
|
|
161
|
-
* ```
|
|
162
|
-
* await dbClient.transaction(async (txClient) => {
|
|
163
|
-
* await txClient.query('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
|
|
164
|
-
* await txClient.query('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
|
|
165
|
-
* });
|
|
166
|
-
* ```
|
|
167
|
-
*/
|
|
168
116
|
async transaction<T>(callback: (txClient: DBClient) => Promise<T>): Promise<T> {
|
|
169
117
|
if (this.isTransactionClient) return callback(this);
|
|
170
118
|
|
|
@@ -212,10 +160,6 @@ export class DBClient {
|
|
|
212
160
|
throw new StabilizeError("Transaction not supported by this client configuration.", "TX_ERROR");
|
|
213
161
|
}
|
|
214
162
|
|
|
215
|
-
/**
|
|
216
|
-
* Closes the database connection pool gracefully.
|
|
217
|
-
* Should be called when the application is shutting down.
|
|
218
|
-
*/
|
|
219
163
|
async close() {
|
|
220
164
|
if (this.client instanceof Database) {
|
|
221
165
|
this.client.close();
|
|
@@ -226,38 +170,20 @@ export class DBClient {
|
|
|
226
170
|
this.logger.logInfo("Database connection closed");
|
|
227
171
|
}
|
|
228
172
|
|
|
229
|
-
/**
|
|
230
|
-
* Executes a SQL query for migrations/transactions that
|
|
231
|
-
* does NOT expect any result rows and returns void.
|
|
232
|
-
* This is used for DDL and transaction statements (e.g. CREATE TABLE, BEGIN, COMMIT)
|
|
233
|
-
* that should never be iterated over.
|
|
234
|
-
*
|
|
235
|
-
* Logs the execution time for each query.
|
|
236
|
-
*
|
|
237
|
-
* @param query The SQL query string with `?` as placeholders.
|
|
238
|
-
* @param params An array of parameters to bind to the query.
|
|
239
|
-
* @returns A promise that resolves when the query has executed.
|
|
240
|
-
* @example
|
|
241
|
-
* ```
|
|
242
|
-
* await dbClient.migrationQuery('CREATE TABLE ...');
|
|
243
|
-
* await dbClient.migrationQuery('BEGIN');
|
|
244
|
-
* await dbClient.migrationQuery('COMMIT');
|
|
245
|
-
* ```
|
|
246
|
-
*/
|
|
247
173
|
async migrationQuery(query: string, params: any[] = []): Promise<void> {
|
|
248
174
|
const start = Date.now();
|
|
249
175
|
this.logger.logQuery(query, params);
|
|
250
176
|
|
|
251
|
-
if (this.client instanceof Database) {
|
|
177
|
+
if (this.client instanceof Database) {
|
|
252
178
|
let stmt = this.preparedStatements.get(query);
|
|
253
179
|
if (!stmt) {
|
|
254
180
|
stmt = this.client.prepare(query);
|
|
255
181
|
this.preparedStatements.set(query, stmt);
|
|
256
182
|
}
|
|
257
183
|
stmt.run(...params);
|
|
258
|
-
} else if (isMySQLPool(this.client) || ('query' in this.client && 'release' in this.client && !(this.client instanceof Pool))) {
|
|
184
|
+
} else if (isMySQLPool(this.client) || ('query' in this.client && 'release' in this.client && !(this.client instanceof Pool))) {
|
|
259
185
|
await (this.client as mysql.Pool).query(query, params);
|
|
260
|
-
} else if (this.config.type = DBType.Postgres) {
|
|
186
|
+
} else if (this.config.type = DBType.Postgres) {
|
|
261
187
|
let paramIndex = 0;
|
|
262
188
|
const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
|
|
263
189
|
await (this.client as Pool).query(pgQuery, params);
|
|
@@ -266,5 +192,4 @@ export class DBClient {
|
|
|
266
192
|
const executionTime = Date.now() - start;
|
|
267
193
|
this.logger.logQuery(query, params, executionTime);
|
|
268
194
|
}
|
|
269
|
-
|
|
270
|
-
}
|
|
195
|
+
}
|
package/hooks.ts
CHANGED
|
@@ -1,33 +1,76 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file hooks.ts
|
|
3
|
+
* @description Provides lifecycle hooks for Stabilize ORM models, integrated with the programmatic API.
|
|
4
|
+
* @author ElectronSz
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { MetadataStorage } from "./model";
|
|
2
8
|
|
|
3
9
|
export type HookType =
|
|
4
|
-
|
|
|
5
|
-
|
|
|
6
|
-
|
|
|
7
|
-
|
|
|
10
|
+
| "beforeCreate"
|
|
11
|
+
| "afterCreate"
|
|
12
|
+
| "beforeUpdate"
|
|
13
|
+
| "afterUpdate"
|
|
14
|
+
| "beforeSave"
|
|
15
|
+
| "afterSave"
|
|
16
|
+
| "beforeDelete"
|
|
17
|
+
| "afterDelete";
|
|
18
|
+
|
|
19
|
+
export type HookCallback = (entity: any) => Promise<void> | void;
|
|
20
|
+
|
|
21
|
+
export interface Hook {
|
|
22
|
+
type: HookType;
|
|
23
|
+
callback: HookCallback;
|
|
24
|
+
}
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
// Extend ModelConfig to include hooks
|
|
27
|
+
declare module "./model" {
|
|
28
|
+
interface ModelConfig {
|
|
29
|
+
hooks?: Record<HookType, HookCallback | HookCallback[]>;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
10
32
|
|
|
11
33
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
34
|
+
* Registers hooks for a model in the MetadataStorage.
|
|
35
|
+
* @param model The model class.
|
|
36
|
+
* @param hooks A record of hook types to their callbacks.
|
|
14
37
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
hooks[type] = hooks[type] || [];
|
|
20
|
-
hooks[type].push(propertyKey);
|
|
21
|
-
Reflect.defineMetadata(HOOK_METADATA_KEY, hooks, target);
|
|
22
|
-
};
|
|
38
|
+
export function registerHooks(model: Function, hooks: Record<HookType, HookCallback | HookCallback[]>) {
|
|
39
|
+
const config = MetadataStorage.getModelMetadata(model) || { tableName: "", columns: {} };
|
|
40
|
+
config.hooks = { ...config.hooks, ...hooks };
|
|
41
|
+
MetadataStorage.setModelMetadata(model, config);
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
/**
|
|
26
|
-
*
|
|
45
|
+
* Retrieves hooks for a given entity and hook type.
|
|
46
|
+
* Combines hooks from MetadataStorage and class methods.
|
|
47
|
+
* @param entity The entity instance.
|
|
48
|
+
* @param type The hook type (e.g., 'beforeCreate').
|
|
49
|
+
* @returns An array of Hook objects to execute.
|
|
27
50
|
*/
|
|
28
|
-
export function getHooks(
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
export function getHooks(entity: any, type: HookType): Hook[] {
|
|
52
|
+
const hooks: Hook[] = [];
|
|
53
|
+
const model = Object.getPrototypeOf(entity).constructor;
|
|
54
|
+
|
|
55
|
+
// Get hooks from MetadataStorage
|
|
56
|
+
const config = MetadataStorage.getModelMetadata(model);
|
|
57
|
+
if (config?.hooks?.[type]) {
|
|
58
|
+
const callbacks = Array.isArray(config.hooks[type])
|
|
59
|
+
? config.hooks[type]
|
|
60
|
+
: [config.hooks[type]];
|
|
61
|
+
hooks.push(...callbacks.map(callback => ({
|
|
62
|
+
type,
|
|
63
|
+
callback: () => callback(entity),
|
|
64
|
+
})));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get hooks from class methods
|
|
68
|
+
if (typeof entity[type] === "function") {
|
|
69
|
+
hooks.push({
|
|
70
|
+
type,
|
|
71
|
+
callback: () => entity[type](),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return hooks;
|
|
33
76
|
}
|
package/index.ts
CHANGED
|
@@ -9,24 +9,6 @@ import { type Logger, ConsoleLogger } from "./logger";
|
|
|
9
9
|
import { QueryBuilder } from "./query-builder";
|
|
10
10
|
import { Repository } from "./repository";
|
|
11
11
|
import { runMigrations, generateMigration, type Migration } from "./migrations";
|
|
12
|
-
import { Hook } from "./hooks";
|
|
13
|
-
import {
|
|
14
|
-
Model,
|
|
15
|
-
Column,
|
|
16
|
-
Required,
|
|
17
|
-
Unique,
|
|
18
|
-
SoftDelete,
|
|
19
|
-
OneToOne,
|
|
20
|
-
ManyToOne,
|
|
21
|
-
OneToMany,
|
|
22
|
-
ManyToMany,
|
|
23
|
-
ModelKey,
|
|
24
|
-
ColumnKey,
|
|
25
|
-
ValidatorKey,
|
|
26
|
-
Versioned,
|
|
27
|
-
RelationKey,
|
|
28
|
-
SoftDeleteKey,
|
|
29
|
-
} from "./decorators";
|
|
30
12
|
import {
|
|
31
13
|
type DBConfig,
|
|
32
14
|
type CacheConfig,
|
|
@@ -40,7 +22,8 @@ import {
|
|
|
40
22
|
type CacheStats,
|
|
41
23
|
LogLevel,
|
|
42
24
|
} from "./types";
|
|
43
|
-
|
|
25
|
+
import { defineModel } from "./model";
|
|
26
|
+
import type { Hook } from "./hooks";
|
|
44
27
|
|
|
45
28
|
export class Stabilize {
|
|
46
29
|
public client: DBClient;
|
|
@@ -68,7 +51,7 @@ export class Stabilize {
|
|
|
68
51
|
|
|
69
52
|
/**
|
|
70
53
|
* Gets a repository for a given model, used to perform CRUD operations.
|
|
71
|
-
* @param model The model class,
|
|
54
|
+
* @param model The model class, defined using `defineModel`.
|
|
72
55
|
* @returns A new `Repository` instance for the specified model.
|
|
73
56
|
* @example
|
|
74
57
|
* ```
|
|
@@ -99,7 +82,6 @@ export class Stabilize {
|
|
|
99
82
|
* try {
|
|
100
83
|
* await stabilize.transaction(async (txClient) => {
|
|
101
84
|
* const newUser = await userRepo.create({ name: 'Ciniso Dlamini' }, {}, txClient);
|
|
102
|
-
* // The new user's ID is needed for the profile, linking the operations.
|
|
103
85
|
* await profileRepo.create({ userId: newUser.id, bio: 'A new bio' }, {}, txClient);
|
|
104
86
|
* });
|
|
105
87
|
* console.log('User and profile created successfully.');
|
|
@@ -151,28 +133,13 @@ export {
|
|
|
151
133
|
Cache,
|
|
152
134
|
ConsoleLogger,
|
|
153
135
|
DBType,
|
|
154
|
-
DataTypes,
|
|
136
|
+
DataTypes,
|
|
155
137
|
LogLevel,
|
|
156
138
|
RelationType,
|
|
157
139
|
StabilizeError,
|
|
158
|
-
Model,
|
|
159
|
-
Column,
|
|
160
|
-
Required,
|
|
161
|
-
Unique,
|
|
162
|
-
SoftDelete,
|
|
163
|
-
Versioned,
|
|
164
|
-
Hook,
|
|
165
|
-
OneToOne,
|
|
166
|
-
ManyToOne,
|
|
167
|
-
OneToMany,
|
|
168
|
-
ManyToMany,
|
|
169
|
-
ModelKey,
|
|
170
|
-
ColumnKey,
|
|
171
|
-
ValidatorKey,
|
|
172
|
-
RelationKey,
|
|
173
|
-
SoftDeleteKey,
|
|
174
140
|
runMigrations,
|
|
175
141
|
generateMigration,
|
|
142
|
+
defineModel,
|
|
176
143
|
};
|
|
177
144
|
|
|
178
145
|
export type {
|
|
@@ -184,4 +151,5 @@ export type {
|
|
|
184
151
|
PoolMetrics,
|
|
185
152
|
CacheStats,
|
|
186
153
|
Logger,
|
|
187
|
-
|
|
154
|
+
Hook
|
|
155
|
+
};
|
package/migrations.ts
CHANGED
|
@@ -6,17 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { DBClient } from "./client";
|
|
9
|
-
import {
|
|
9
|
+
import { MetadataStorage } from "./model";
|
|
10
10
|
import { type DBConfig, type Migration, StabilizeError, DBType, DataTypes } from "./types";
|
|
11
11
|
|
|
12
|
-
type ColumnData = { name: string; type: string };
|
|
13
|
-
type ColumnMetadata = Record<string, ColumnData>;
|
|
14
|
-
type ValidatorMetadata = Record<string, string[]>;
|
|
15
|
-
|
|
16
12
|
/**
|
|
17
13
|
* @internal
|
|
18
14
|
* Formats a SQL query with placeholders for the target database dialect.
|
|
19
|
-
* Replaces '?' with '$1', '$2', etc. for PostgreSQL.
|
|
20
15
|
* @param query The SQL query string with '?' placeholders.
|
|
21
16
|
* @param dbType The target database dialect.
|
|
22
17
|
* @returns The formatted SQL query string.
|
|
@@ -30,17 +25,10 @@ function formatQuery(query: string, dbType: DBType): string {
|
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
/**
|
|
33
|
-
* Maps an abstract data type
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* converting each logical type to its proper SQL type in CREATE TABLE migrations.
|
|
38
|
-
*
|
|
39
|
-
* @param dt - The data type to map. Accepts either a value from the DataTypes enum, or a string
|
|
40
|
-
* (e.g., "string", "integer", "boolean", etc.).
|
|
41
|
-
* @param dbType - The target database dialect (DBType.Postgres, DBType.MySQL, or DBType.SQLite).
|
|
42
|
-
* @returns The SQL column type string appropriate for the database and logical type.
|
|
43
|
-
*
|
|
28
|
+
* Maps an abstract data type to the correct SQL type string for the specified database dialect.
|
|
29
|
+
* @param dt The data type to map.
|
|
30
|
+
* @param dbType The target database dialect.
|
|
31
|
+
* @returns The SQL column type string.
|
|
44
32
|
*/
|
|
45
33
|
function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
|
|
46
34
|
let type: string;
|
|
@@ -86,7 +74,6 @@ function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
|
|
|
86
74
|
default: return "TEXT";
|
|
87
75
|
}
|
|
88
76
|
}
|
|
89
|
-
// SQLite
|
|
90
77
|
if (dbType === DBType.SQLite) {
|
|
91
78
|
switch (type) {
|
|
92
79
|
case "string": return "TEXT";
|
|
@@ -127,14 +114,10 @@ function getAutoIncrementPK(dbType: DBType): string {
|
|
|
127
114
|
}
|
|
128
115
|
|
|
129
116
|
/**
|
|
130
|
-
* Generates SQL migration scripts (`up` and `down`) based on a model's
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* @param model The model class decorated with `@Model` and `@Column`.
|
|
136
|
-
* @param name A descriptive name for the migration (used for the migration object).
|
|
137
|
-
* @param dbType The target database dialect to generate SQL for. Defaults to Postgres.
|
|
117
|
+
* Generates SQL migration scripts (`up` and `down`) based on a model's configuration.
|
|
118
|
+
* @param model The model class defined with `defineModel`.
|
|
119
|
+
* @param name A descriptive name for the migration.
|
|
120
|
+
* @param dbType The target database dialect.
|
|
138
121
|
* @returns A promise that resolves to a `Migration` object containing the `up` and `down` SQL scripts.
|
|
139
122
|
*/
|
|
140
123
|
export async function generateMigration(
|
|
@@ -142,14 +125,14 @@ export async function generateMigration(
|
|
|
142
125
|
name: string,
|
|
143
126
|
dbType: DBType,
|
|
144
127
|
): Promise<Migration> {
|
|
145
|
-
const tableName =
|
|
128
|
+
const tableName = MetadataStorage.getTableName(model);
|
|
146
129
|
if (!tableName) {
|
|
147
|
-
throw new StabilizeError("Model not
|
|
130
|
+
throw new StabilizeError("Model not defined with tableName", "MIGRATION_ERROR");
|
|
148
131
|
}
|
|
149
132
|
|
|
150
|
-
const columns
|
|
151
|
-
const validators
|
|
152
|
-
const versioned
|
|
133
|
+
const columns = MetadataStorage.getColumns(model);
|
|
134
|
+
const validators = MetadataStorage.getValidators(model);
|
|
135
|
+
const versioned = MetadataStorage.isVersioned(model);
|
|
153
136
|
|
|
154
137
|
const columnDefs: string[] = [];
|
|
155
138
|
|
|
@@ -160,7 +143,7 @@ export async function generateMigration(
|
|
|
160
143
|
defParts.push("id");
|
|
161
144
|
defParts.push(getAutoIncrementPK(dbType));
|
|
162
145
|
} else {
|
|
163
|
-
defParts.push(col.name);
|
|
146
|
+
defParts.push(col.name || key);
|
|
164
147
|
defParts.push(mapDataTypeToSql(col.type, dbType));
|
|
165
148
|
}
|
|
166
149
|
|
|
@@ -170,6 +153,12 @@ export async function generateMigration(
|
|
|
170
153
|
if (validators[key]?.includes("unique")) {
|
|
171
154
|
defParts.push("UNIQUE");
|
|
172
155
|
}
|
|
156
|
+
if (col.defaultValue !== undefined) {
|
|
157
|
+
defParts.push(`DEFAULT ${JSON.stringify(col.defaultValue)}`);
|
|
158
|
+
}
|
|
159
|
+
if (col.index) {
|
|
160
|
+
defParts.push(`INDEX ${col.index}`);
|
|
161
|
+
}
|
|
173
162
|
|
|
174
163
|
columnDefs.push(defParts.join(" "));
|
|
175
164
|
}
|
|
@@ -177,7 +166,6 @@ export async function generateMigration(
|
|
|
177
166
|
const up: string[] = [`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`];
|
|
178
167
|
const down: string[] = [`DROP TABLE IF EXISTS ${tableName}`];
|
|
179
168
|
|
|
180
|
-
// If model is versioned, add history table migration
|
|
181
169
|
if (versioned) {
|
|
182
170
|
const [historyUp, historyDown] = generateHistoryMigration(tableName, columnDefs, dbType);
|
|
183
171
|
up.push(historyUp);
|
|
@@ -223,7 +211,7 @@ function generateHistoryMigration(
|
|
|
223
211
|
|
|
224
212
|
/**
|
|
225
213
|
* @internal
|
|
226
|
-
* Gets the database-specific SQL for creating the `migrations` table
|
|
214
|
+
* Gets the database-specific SQL for creating the `migrations` table.
|
|
227
215
|
* @param dbType The target database dialect.
|
|
228
216
|
* @returns The SQL string for the `CREATE TABLE` statement.
|
|
229
217
|
*/
|
|
@@ -253,9 +241,6 @@ function getMigrationsTableSQL(dbType: DBType): string {
|
|
|
253
241
|
|
|
254
242
|
/**
|
|
255
243
|
* Connects to the database and runs all pending migrations.
|
|
256
|
-
* It tracks which migrations have been applied by using a `migrations` table in the database.
|
|
257
|
-
* Each migration is run within a transaction to ensure atomicity.
|
|
258
|
-
*
|
|
259
244
|
* @param config The database configuration object.
|
|
260
245
|
* @param migrations An array of `Migration` objects to be executed.
|
|
261
246
|
*/
|
|
@@ -293,4 +278,4 @@ export async function runMigrations(config: DBConfig, migrations: Migration[]) {
|
|
|
293
278
|
}
|
|
294
279
|
}
|
|
295
280
|
|
|
296
|
-
export type { Migration };
|
|
281
|
+
export type { Migration };
|
package/model.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @file model.ts
|
|
4
|
+
* @description Provides a programmatic API for defining models and a metadata storage system.
|
|
5
|
+
* @author ElectronSz
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DataTypes, RelationType, DBType } from './types';
|
|
9
|
+
|
|
10
|
+
// Interface for column configuration
|
|
11
|
+
export interface ColumnConfig {
|
|
12
|
+
name?: string;
|
|
13
|
+
type: DataTypes;
|
|
14
|
+
length?: number;
|
|
15
|
+
precision?: number;
|
|
16
|
+
scale?: number;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
unique?: boolean;
|
|
19
|
+
defaultValue?: any;
|
|
20
|
+
index?: string; // Optional index name
|
|
21
|
+
softDelete?: boolean; // Marks column as soft delete field
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Interface for relationship configuration
|
|
25
|
+
export interface RelationConfig {
|
|
26
|
+
type: RelationType;
|
|
27
|
+
target: () => any; // Reference to another model
|
|
28
|
+
property: string; // Property name in the model
|
|
29
|
+
foreignKey?: string;
|
|
30
|
+
inverseKey?: string;
|
|
31
|
+
joinTable?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Interface for model configuration
|
|
35
|
+
export interface ModelConfig {
|
|
36
|
+
tableName: string;
|
|
37
|
+
versioned?: boolean;
|
|
38
|
+
softDelete?: boolean;
|
|
39
|
+
columns: Record<string, ColumnConfig>;
|
|
40
|
+
relations?: RelationConfig[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Metadata storage for models
|
|
44
|
+
export class MetadataStorage {
|
|
45
|
+
private static models: Map<Function, ModelConfig> = new Map();
|
|
46
|
+
|
|
47
|
+
static setModelMetadata(model: Function, config: ModelConfig) {
|
|
48
|
+
this.models.set(model, config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static getModelMetadata(model: Function): ModelConfig | undefined {
|
|
52
|
+
return this.models.get(model);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static getTableName(model: Function): string {
|
|
56
|
+
return this.getModelMetadata(model)?.tableName || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static getColumns(model: Function): Record<string, ColumnConfig> {
|
|
60
|
+
return this.getModelMetadata(model)?.columns || {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static getValidators(model: Function): Record<string, string[]> {
|
|
64
|
+
const columns = this.getModelMetadata(model)?.columns || {};
|
|
65
|
+
const validators: Record<string, string[]> = {};
|
|
66
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
67
|
+
const rules: string[] = [];
|
|
68
|
+
if (col.required) rules.push('required');
|
|
69
|
+
if (col.unique) rules.push('unique');
|
|
70
|
+
validators[key] = rules;
|
|
71
|
+
}
|
|
72
|
+
return validators;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static getRelations(model: Function): Record<string, RelationConfig> {
|
|
76
|
+
const relations = this.getModelMetadata(model)?.relations || [];
|
|
77
|
+
const result: Record<string, RelationConfig> = {};
|
|
78
|
+
for (const rel of relations) {
|
|
79
|
+
result[rel.property] = rel;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static getSoftDeleteField(model: Function): string | null {
|
|
85
|
+
const columns = this.getModelMetadata(model)?.columns || {};
|
|
86
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
87
|
+
if (col.softDelete) return key;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static isVersioned(model: Function): boolean {
|
|
93
|
+
return !!this.getModelMetadata(model)?.versioned;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Programmatic model definition
|
|
98
|
+
export function defineModel(config: ModelConfig) {
|
|
99
|
+
class Model {
|
|
100
|
+
constructor(data: any) {
|
|
101
|
+
Object.assign(this, data);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Store metadata
|
|
106
|
+
MetadataStorage.setModelMetadata(Model, {
|
|
107
|
+
tableName: config.tableName,
|
|
108
|
+
versioned: config.versioned || false,
|
|
109
|
+
softDelete: config.softDelete || false,
|
|
110
|
+
columns: config.columns,
|
|
111
|
+
relations: config.relations || [],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return Model;
|
|
115
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stabilize-orm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
7
|
-
"bin": {
|
|
8
|
-
"stabilize": "dist/cli/stabilize-cli.js"
|
|
9
|
-
},
|
|
10
7
|
"scripts": {
|
|
11
|
-
"build": "bun build src/index.ts --outdir dist --target bun --minify
|
|
8
|
+
"build": "bun build src/index.ts --outdir dist --target bun --minify",
|
|
12
9
|
"prepublishOnly": "bun run build && bun run test",
|
|
13
|
-
"
|
|
14
|
-
"format": "bunx prettier --write .",
|
|
15
|
-
"lint": "eslint src tests cli examples",
|
|
16
|
-
"cli": "bun run cli/stabilize-cli.ts"
|
|
10
|
+
"format": "bunx prettier --write ."
|
|
17
11
|
},
|
|
18
12
|
"keywords": [
|
|
19
13
|
"orm",
|