stabilize-orm 1.1.8 → 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/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) { // SQLite
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)) { // MySQL
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 ) { // 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) { // SQLite
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))) { // mysql2 Pool or Connection
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) { // Postgres Pool or Client
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 ADDED
@@ -0,0 +1,76 @@
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";
8
+
9
+ export type HookType =
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
+ }
25
+
26
+ // Extend ModelConfig to include hooks
27
+ declare module "./model" {
28
+ interface ModelConfig {
29
+ hooks?: Record<HookType, HookCallback | HookCallback[]>;
30
+ }
31
+ }
32
+
33
+ /**
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.
37
+ */
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);
42
+ }
43
+
44
+ /**
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.
50
+ */
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;
76
+ }
package/index.ts CHANGED
@@ -9,28 +9,12 @@ 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 {
13
- Model,
14
- Column,
15
- Required,
16
- Unique,
17
- SoftDelete,
18
- OneToOne,
19
- ManyToOne,
20
- OneToMany,
21
- ManyToMany,
22
- ModelKey,
23
- ColumnKey,
24
- ValidatorKey,
25
- RelationKey,
26
- SoftDeleteKey,
27
- } from "./decorators";
28
12
  import {
29
13
  type DBConfig,
30
14
  type CacheConfig,
31
15
  type LoggerConfig,
32
16
  DBType,
33
- DataTypes, // --- FIX: Import DataTypes here ---
17
+ DataTypes,
34
18
  StabilizeError,
35
19
  type PoolMetrics,
36
20
  type QueryHint,
@@ -38,7 +22,8 @@ import {
38
22
  type CacheStats,
39
23
  LogLevel,
40
24
  } from "./types";
41
-
25
+ import { defineModel } from "./model";
26
+ import type { Hook } from "./hooks";
42
27
 
43
28
  export class Stabilize {
44
29
  public client: DBClient;
@@ -66,7 +51,7 @@ export class Stabilize {
66
51
 
67
52
  /**
68
53
  * Gets a repository for a given model, used to perform CRUD operations.
69
- * @param model The model class, which must be decorated with `@Model`.
54
+ * @param model The model class, defined using `defineModel`.
70
55
  * @returns A new `Repository` instance for the specified model.
71
56
  * @example
72
57
  * ```
@@ -97,7 +82,6 @@ export class Stabilize {
97
82
  * try {
98
83
  * await stabilize.transaction(async (txClient) => {
99
84
  * const newUser = await userRepo.create({ name: 'Ciniso Dlamini' }, {}, txClient);
100
- * // The new user's ID is needed for the profile, linking the operations.
101
85
  * await profileRepo.create({ userId: newUser.id, bio: 'A new bio' }, {}, txClient);
102
86
  * });
103
87
  * console.log('User and profile created successfully.');
@@ -149,26 +133,13 @@ export {
149
133
  Cache,
150
134
  ConsoleLogger,
151
135
  DBType,
152
- DataTypes,
136
+ DataTypes,
153
137
  LogLevel,
154
138
  RelationType,
155
139
  StabilizeError,
156
- Model,
157
- Column,
158
- Required,
159
- Unique,
160
- SoftDelete,
161
- OneToOne,
162
- ManyToOne,
163
- OneToMany,
164
- ManyToMany,
165
- ModelKey,
166
- ColumnKey,
167
- ValidatorKey,
168
- RelationKey,
169
- SoftDeleteKey,
170
140
  runMigrations,
171
141
  generateMigration,
142
+ defineModel,
172
143
  };
173
144
 
174
145
  export type {
@@ -180,4 +151,5 @@ export type {
180
151
  PoolMetrics,
181
152
  CacheStats,
182
153
  Logger,
183
- };
154
+ Hook
155
+ };
package/migrations.ts CHANGED
@@ -6,18 +6,12 @@
6
6
  */
7
7
 
8
8
  import { DBClient } from "./client";
9
- import { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey } from "./decorators";
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
- // --- FIX: New Helper Function to format queries for different DBs ---
17
12
  /**
18
13
  * @internal
19
14
  * Formats a SQL query with placeholders for the target database dialect.
20
- * Replaces '?' with '$1', '$2', etc. for PostgreSQL.
21
15
  * @param query The SQL query string with '?' placeholders.
22
16
  * @param dbType The target database dialect.
23
17
  * @returns The formatted SQL query string.
@@ -30,19 +24,11 @@ function formatQuery(query: string, dbType: DBType): string {
30
24
  return query;
31
25
  }
32
26
 
33
-
34
27
  /**
35
- * Maps an abstract data type (from DataTypes enum or a string) to the correct SQL type string
36
- * for the specified database dialect (Postgres, MySQL, or SQLite).
37
- *
38
- * This function enables model definitions to be portable across different databases by
39
- * converting each logical type to its proper SQL type in CREATE TABLE migrations.
40
- *
41
- * @param dt - The data type to map. Accepts either a value from the DataTypes enum, or a string
42
- * (e.g., "string", "integer", "boolean", etc.).
43
- * @param dbType - The target database dialect (DBType.Postgres, DBType.MySQL, or DBType.SQLite).
44
- * @returns The SQL column type string appropriate for the database and logical type.
45
- *
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.
46
32
  */
47
33
  function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
48
34
  let type: string;
@@ -88,7 +74,6 @@ function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
88
74
  default: return "TEXT";
89
75
  }
90
76
  }
91
- // SQLite
92
77
  if (dbType === DBType.SQLite) {
93
78
  switch (type) {
94
79
  case "string": return "TEXT";
@@ -110,7 +95,6 @@ function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
110
95
  return "TEXT";
111
96
  }
112
97
 
113
-
114
98
  /**
115
99
  * @internal
116
100
  * Gets the database-specific SQL for an auto-incrementing primary key.
@@ -130,48 +114,10 @@ function getAutoIncrementPK(dbType: DBType): string {
130
114
  }
131
115
 
132
116
  /**
133
- * @internal
134
- * Gets the database-specific SQL for a timestamp column.
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.
135
120
  * @param dbType The target database dialect.
136
- * @returns The SQL string for the timestamp column type.
137
- */
138
- function getTimestampType(dbType: DBType): string {
139
- switch (dbType) {
140
- case DBType.Postgres:
141
- return "TIMESTAMP";
142
- case DBType.MySQL:
143
- return "DATETIME";
144
- case DBType.SQLite:
145
- default:
146
- return "TEXT";
147
- }
148
- }
149
-
150
- /**
151
- * @internal
152
- * Gets the database-specific SQL for a default `CURRENT_TIMESTAMP` value.
153
- * @param dbType The target database dialect.
154
- * @returns The SQL string for the default value.
155
- */
156
- function getTimestampDefault(dbType: DBType): string {
157
- switch (dbType) {
158
- case DBType.Postgres:
159
- case DBType.SQLite:
160
- return "DEFAULT CURRENT_TIMESTAMP";
161
- case DBType.MySQL:
162
- return "DEFAULT CURRENT_TIMESTAMP";
163
- default:
164
- return "DEFAULT CURRENT_TIMESTAMP";
165
- }
166
- }
167
-
168
- /**
169
- * Generates SQL migration scripts (`up` and `down`) based on a model's decorators.
170
- * This function reads the metadata from a model class to create a `CREATE TABLE` statement.
171
- *
172
- * @param model The model class decorated with `@Model` and `@Column`.
173
- * @param name A descriptive name for the migration (used for the migration object).
174
- * @param dbType The target database dialect to generate SQL for. Defaults to Postgres.
175
121
  * @returns A promise that resolves to a `Migration` object containing the `up` and `down` SQL scripts.
176
122
  */
177
123
  export async function generateMigration(
@@ -179,13 +125,14 @@ export async function generateMigration(
179
125
  name: string,
180
126
  dbType: DBType,
181
127
  ): Promise<Migration> {
182
- const tableName = Reflect.getMetadata(ModelKey, model);
128
+ const tableName = MetadataStorage.getTableName(model);
183
129
  if (!tableName) {
184
- throw new StabilizeError("Model not decorated with @Model", "MIGRATION_ERROR");
130
+ throw new StabilizeError("Model not defined with tableName", "MIGRATION_ERROR");
185
131
  }
186
132
 
187
- const columns: ColumnMetadata = Reflect.getMetadata(ColumnKey, model.prototype) || {};
188
- const validators: ValidatorMetadata = Reflect.getMetadata(ValidatorKey, model.prototype) || {};
133
+ const columns = MetadataStorage.getColumns(model);
134
+ const validators = MetadataStorage.getValidators(model);
135
+ const versioned = MetadataStorage.isVersioned(model);
189
136
 
190
137
  const columnDefs: string[] = [];
191
138
 
@@ -196,7 +143,7 @@ export async function generateMigration(
196
143
  defParts.push("id");
197
144
  defParts.push(getAutoIncrementPK(dbType));
198
145
  } else {
199
- defParts.push(col.name);
146
+ defParts.push(col.name || key);
200
147
  defParts.push(mapDataTypeToSql(col.type, dbType));
201
148
  }
202
149
 
@@ -206,19 +153,65 @@ export async function generateMigration(
206
153
  if (validators[key]?.includes("unique")) {
207
154
  defParts.push("UNIQUE");
208
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
+ }
209
162
 
210
163
  columnDefs.push(defParts.join(" "));
211
164
  }
212
165
 
213
- const up = [`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`];
214
- const down = [`DROP TABLE IF EXISTS ${tableName}`];
166
+ const up: string[] = [`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`];
167
+ const down: string[] = [`DROP TABLE IF EXISTS ${tableName}`];
168
+
169
+ if (versioned) {
170
+ const [historyUp, historyDown] = generateHistoryMigration(tableName, columnDefs, dbType);
171
+ up.push(historyUp);
172
+ down.push(historyDown);
173
+ }
215
174
 
216
175
  return { up, down, name };
217
176
  }
218
177
 
178
+ /**
179
+ * Generates SQL for a version/audit history table for time-travel queries.
180
+ * @param tableName The name of the main table.
181
+ * @param columnDefs The column definitions (from the main table).
182
+ * @param dbType The target database dialect.
183
+ */
184
+ function generateHistoryMigration(
185
+ tableName: string,
186
+ columnDefs: string[],
187
+ dbType: DBType,
188
+ ): [string, string] {
189
+ const historyTable = `${tableName}_history`;
190
+ let opType = "VARCHAR(10) NOT NULL";
191
+ let versionType = "INT NOT NULL";
192
+ let tsType = dbType === DBType.MySQL ? "DATETIME" :
193
+ dbType === DBType.SQLite ? "TEXT" : "TIMESTAMP";
194
+ let modByType = dbType === DBType.MySQL ? "VARCHAR(255)" : "TEXT";
195
+ let modAtType = tsType + (dbType === DBType.Postgres ? " DEFAULT CURRENT_TIMESTAMP" : "");
196
+
197
+ const historyColumns = [
198
+ ...columnDefs,
199
+ `operation ${opType}`,
200
+ `version ${versionType}`,
201
+ `valid_from ${tsType} NOT NULL`,
202
+ `valid_to ${tsType}`,
203
+ `modified_by ${modByType}`,
204
+ `modified_at ${modAtType}`
205
+ ];
206
+ return [
207
+ `CREATE TABLE IF NOT EXISTS ${historyTable} (${historyColumns.join(", ")})`,
208
+ `DROP TABLE IF EXISTS ${historyTable}`
209
+ ];
210
+ }
211
+
219
212
  /**
220
213
  * @internal
221
- * Gets the database-specific SQL for creating the `migrations` table, which tracks applied migrations.
214
+ * Gets the database-specific SQL for creating the `migrations` table.
222
215
  * @param dbType The target database dialect.
223
216
  * @returns The SQL string for the `CREATE TABLE` statement.
224
217
  */
@@ -248,9 +241,6 @@ function getMigrationsTableSQL(dbType: DBType): string {
248
241
 
249
242
  /**
250
243
  * Connects to the database and runs all pending migrations.
251
- * It tracks which migrations have been applied by using a `migrations` table in the database.
252
- * Each migration is run within a transaction to ensure atomicity.
253
- *
254
244
  * @param config The database configuration object.
255
245
  * @param migrations An array of `Migration` objects to be executed.
256
246
  */
@@ -288,4 +278,4 @@ export async function runMigrations(config: DBConfig, migrations: Migration[]) {
288
278
  }
289
279
  }
290
280
 
291
- 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
+ }