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/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 CHANGED
@@ -1,33 +1,76 @@
1
- import 'reflect-metadata';
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
- | 'beforeCreate' | 'afterCreate'
5
- | 'beforeUpdate' | 'afterUpdate'
6
- | 'beforeDelete' | 'afterDelete'
7
- | 'beforeSave' | 'afterSave';
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
- const HOOK_METADATA_KEY = Symbol('stabilize:hooks');
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
- * Decorator to mark a method as a lifecycle hook.
13
- * Usage: @Hook('beforeCreate')
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 Hook(type: HookType) {
16
- return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
17
- const hooks: Record<HookType, string[]> =
18
- Reflect.getMetadata(HOOK_METADATA_KEY, target) || {};
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
- * Get hooks of a specific type for a model instance.
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(instance: any, type: HookType): Array<() => Promise<void> | void> {
29
- const proto = Object.getPrototypeOf(instance);
30
- const hooks: Record<HookType, string[]> =
31
- Reflect.getMetadata(HOOK_METADATA_KEY, proto) || {};
32
- return (hooks[type] || []).map((methodName) => instance[methodName].bind(instance));
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, which must be decorated with `@Model`.
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 { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey, VersionedKey } 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
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 (from DataTypes enum or a string) to the correct SQL type string
34
- * for the specified database dialect (Postgres, MySQL, or SQLite).
35
- *
36
- * This function enables model definitions to be portable across different databases by
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 decorators.
131
- * This function reads the metadata from a model class to create a `CREATE TABLE` statement.
132
- *
133
- * If the model is versioned (has @Versioned), also generates a history table for time-travel queries.
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 = Reflect.getMetadata(ModelKey, model);
128
+ const tableName = MetadataStorage.getTableName(model);
146
129
  if (!tableName) {
147
- throw new StabilizeError("Model not decorated with @Model", "MIGRATION_ERROR");
130
+ throw new StabilizeError("Model not defined with tableName", "MIGRATION_ERROR");
148
131
  }
149
132
 
150
- const columns: ColumnMetadata = Reflect.getMetadata(ColumnKey, model.prototype) || {};
151
- const validators: ValidatorMetadata = Reflect.getMetadata(ValidatorKey, model.prototype) || {};
152
- const versioned: boolean = !!Reflect.getMetadata(VersionedKey, model);
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, which tracks applied migrations.
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.2.0",
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 && bun build cli/stabilize-cli.ts --outdir dist/cli --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
- "build:exe": "bun build cli/stabilize-cli.ts --compile --outfile dist/stabilize-cli --target bun",
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",