stabilize-orm 1.2.0 → 1.3.2

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/model.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @file model.ts
3
+ * @description Provides a programmatic API for defining models and a metadata storage system.
4
+ * @author ElectronSz
5
+ */
6
+
7
+ import type { QueryBuilder } from './query-builder';
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
+ scopes?: Record<string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>>; // Custom query scopes
42
+ }
43
+
44
+ /**
45
+ * Metadata storage for models.
46
+ * Stores and retrieves model configuration such as columns, relations, scopes, etc.
47
+ */
48
+ export class MetadataStorage {
49
+ private static models: Map<Function, ModelConfig> = new Map();
50
+
51
+ /**
52
+ * Associates model metadata with a class constructor.
53
+ * @param model - The class constructor for the model.
54
+ * @param config - The model configuration object.
55
+ */
56
+ static setModelMetadata(model: Function, config: ModelConfig) {
57
+ this.models.set(model, config);
58
+ }
59
+
60
+ /**
61
+ * Retrieves the model configuration for a given model class.
62
+ * @param model - The class constructor for the model.
63
+ * @returns The model configuration or undefined if not found.
64
+ */
65
+ static getModelMetadata(model: Function): ModelConfig | undefined {
66
+ return this.models.get(model);
67
+ }
68
+
69
+ /**
70
+ * Gets the table name for a given model class.
71
+ * @param model - The class constructor for the model.
72
+ * @returns The table name or an empty string if not found.
73
+ */
74
+ static getTableName(model: Function): string {
75
+ return this.getModelMetadata(model)?.tableName || '';
76
+ }
77
+
78
+ /**
79
+ * Gets the column configuration for a given model class.
80
+ * @param model - The class constructor for the model.
81
+ * @returns Record of column names to their configuration.
82
+ */
83
+ static getColumns(model: Function): Record<string, ColumnConfig> {
84
+ return this.getModelMetadata(model)?.columns || {};
85
+ }
86
+
87
+ /**
88
+ * Collects validation rules for each column of a given model.
89
+ * @param model - The class constructor for the model.
90
+ * @returns An object mapping column names to an array of validation rule names.
91
+ */
92
+ static getValidators(model: Function): Record<string, string[]> {
93
+ const columns = this.getModelMetadata(model)?.columns || {};
94
+ const validators: Record<string, string[]> = {};
95
+ for (const [key, col] of Object.entries(columns)) {
96
+ const rules: string[] = [];
97
+ if (col.required) rules.push('required');
98
+ if (col.unique) rules.push('unique');
99
+ validators[key] = rules;
100
+ }
101
+ return validators;
102
+ }
103
+
104
+ /**
105
+ * Gets the relationship configuration for a given model class.
106
+ * @param model - The class constructor for the model.
107
+ * @returns Record of property names to their relation configuration.
108
+ */
109
+ static getRelations(model: Function): Record<string, RelationConfig> {
110
+ const relations = this.getModelMetadata(model)?.relations || [];
111
+ const result: Record<string, RelationConfig> = {};
112
+ for (const rel of relations) {
113
+ result[rel.property] = rel;
114
+ }
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Finds the soft delete field, if any, for a given model class.
120
+ * @param model - The class constructor for the model.
121
+ * @returns The key of the soft delete field, or null if not found.
122
+ */
123
+ static getSoftDeleteField(model: Function): string | null {
124
+ const columns = this.getModelMetadata(model)?.columns || {};
125
+ for (const [key, col] of Object.entries(columns)) {
126
+ if (col.softDelete) return key;
127
+ }
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Checks if the model is versioned.
133
+ * @param model - The class constructor for the model.
134
+ * @returns True if versioned, false otherwise.
135
+ */
136
+ static isVersioned(model: Function): boolean {
137
+ return !!this.getModelMetadata(model)?.versioned;
138
+ }
139
+
140
+ /**
141
+ * Gets custom query scopes for a given model class.
142
+ * @param model - The class constructor for the model.
143
+ * @returns Record of scope names to scope functions.
144
+ */
145
+ static getScopes(model: Function): Record<string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>> {
146
+ return this.getModelMetadata(model)?.scopes || {};
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Programmatically defines a model and stores its metadata.
152
+ * @param config - The model configuration object.
153
+ * @returns The dynamically created model class.
154
+ */
155
+ export function defineModel(config: ModelConfig) {
156
+ class Model {
157
+ /**
158
+ * Constructs a model instance from plain data.
159
+ * @param data - The plain object to assign properties from.
160
+ */
161
+ constructor(data: any) {
162
+ Object.assign(this, data);
163
+ }
164
+ }
165
+
166
+ // Store metadata
167
+ MetadataStorage.setModelMetadata(Model, {
168
+ tableName: config.tableName,
169
+ versioned: config.versioned || false,
170
+ softDelete: config.softDelete || false,
171
+ columns: config.columns,
172
+ relations: config.relations || [],
173
+ scopes: config.scopes || {},
174
+ });
175
+
176
+ return Model;
177
+ }
package/package.json CHANGED
@@ -1,19 +1,13 @@
1
1
  {
2
2
  "name": "stabilize-orm",
3
- "version": "1.2.0",
3
+ "version": "1.3.2",
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",
package/query-builder.ts CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { DBClient } from "./client";
8
8
  import { Cache } from "./cache";
9
+ import { MetadataStorage } from "./model";
10
+ import { StabilizeError } from "./types";
9
11
 
10
12
  /**
11
13
  * A fluent interface for building SQL SELECT queries.
@@ -125,7 +127,7 @@ export class QueryBuilder<T> {
125
127
  */
126
128
  build(): { query: string; params: any[] } {
127
129
  let query = `SELECT ${this.selectFields.join(", ")} FROM ${this.table}`;
128
-
130
+
129
131
  if (this.joins.length > 0) {
130
132
  query += " " + this.joins.join(" ");
131
133
  }
@@ -166,7 +168,7 @@ export class QueryBuilder<T> {
166
168
  cacheKey?: string,
167
169
  ): Promise<T[]> {
168
170
  const { query, params } = this.build();
169
-
171
+
170
172
  // Attempt to retrieve from cache first (cache-aside read)
171
173
  if (cache && cacheKey) {
172
174
  const cached = await cache.get<T[]>(cacheKey);
@@ -178,9 +180,30 @@ export class QueryBuilder<T> {
178
180
 
179
181
  // Store the database results in the cache for future requests
180
182
  if (cache && cacheKey && results.length > 0) {
181
- await cache.set(cacheKey, results, 60);
183
+ await cache.set(cacheKey, results, 60);
182
184
  }
183
-
185
+
184
186
  return results;
185
187
  }
188
+
189
+ /**
190
+ * Applies a named scope to the current query builder.
191
+ *
192
+ * This method looks up a scope function by name for the current model (based on the table name),
193
+ * then invokes the scope function with the query builder and any additional arguments.
194
+ *
195
+ * @param {string} name - The name of the scope to apply.
196
+ * @param {...any} args - Additional arguments to pass to the scope function.
197
+ * @throws {StabilizeError} If no model is found for the current table, or if the specified scope does not exist.
198
+ * @returns {QueryBuilder<T>} The query builder instance after applying the scope.
199
+ */
200
+ scope(name: string, ...args: any[]): QueryBuilder<T> {
201
+ const model = Object.values(MetadataStorage['models']).find(m => m.tableName === this.table)?.constructor;
202
+ if (!model) throw new StabilizeError(`Model for table ${this.table} not found`, "SCOPE_ERROR");
203
+ const scopes = MetadataStorage.getScopes(model);
204
+ const scopeFn = scopes[name];
205
+ if (!scopeFn) throw new StabilizeError(`Scope ${name} not found`, "SCOPE_ERROR");
206
+ return scopeFn(this, ...args);
207
+ }
208
+
186
209
  }
package/repository.ts CHANGED
@@ -9,24 +9,17 @@ import { DBClient } from "./client";
9
9
  import { ConsoleLogger, type Logger } from "./logger";
10
10
  import { QueryBuilder } from "./query-builder";
11
11
  import {
12
+ DataTypes,
12
13
  DBType,
13
14
  RelationType,
14
15
  StabilizeError,
15
16
  type CacheConfig,
16
17
  } from "./types";
17
- import {
18
- ModelKey,
19
- ColumnKey,
20
- ValidatorKey,
21
- RelationKey,
22
- SoftDeleteKey,
23
- VersionedKey,
24
- } from "./decorators";
18
+ import { MetadataStorage } from "./model";
25
19
  import { getHooks, type HookType } from "./hooks";
26
20
 
27
21
  type VersionOperation = "insert" | "update" | "delete";
28
22
 
29
-
30
23
  /**
31
24
  * Provides a generic repository for a model `T`.
32
25
  * This class abstracts the database interactions for a specific model,
@@ -57,7 +50,7 @@ export class Repository<T> {
57
50
  /**
58
51
  * Creates an instance of Repository.
59
52
  * @param client The database client instance for executing queries.
60
- * @param model The model class constructor, decorated with `@Model`.
53
+ * @param model The model class constructor, defined with `defineModel`.
61
54
  * @param cacheConfig Optional configuration for caching.
62
55
  * @param logger A logger instance for logging messages.
63
56
  */
@@ -69,14 +62,30 @@ export class Repository<T> {
69
62
  ) {
70
63
  this.client = client;
71
64
  this.cache = cacheConfig.enabled ? new Cache(cacheConfig, logger) : null;
72
- this.table = Reflect.getMetadata(ModelKey, model) || "";
73
- this.columns = Reflect.getMetadata(ColumnKey, model.prototype) || {};
74
- this.validators = Reflect.getMetadata(ValidatorKey, model.prototype) || {};
75
- this.relations = Reflect.getMetadata(RelationKey, model.prototype) || {};
76
- this.softDeleteField =
77
- Reflect.getMetadata(SoftDeleteKey, model.prototype) || null;
65
+ this.table = MetadataStorage.getTableName(model);
66
+ this.columns = Object.fromEntries(
67
+ Object.entries(MetadataStorage.getColumns(model)).map(([key, col]) => [
68
+ key,
69
+ { name: col.name ?? key, type: typeof col.type === 'string' ? col.type : DataTypes[col.type] },
70
+ ])
71
+ );
72
+ this.relations = Object.fromEntries(
73
+ Object.entries(MetadataStorage.getRelations(model)).map(([key, rel]) => [
74
+ key,
75
+ {
76
+ type: rel.type,
77
+ targetModel: rel.target,
78
+ foreignKey: rel.foreignKey,
79
+ inverseKey: rel.inverseKey,
80
+ joinTable: rel.joinTable,
81
+ },
82
+ ])
83
+ );
84
+ this.validators = MetadataStorage.getValidators(model);
85
+
86
+ this.softDeleteField = MetadataStorage.getSoftDeleteField(model);
78
87
  this.logger = logger;
79
- this.versioned = !!Reflect.getMetadata(VersionedKey, model);
88
+ this.versioned = MetadataStorage.isVersioned(model);
80
89
  this.historyTable = `${this.table}_history`;
81
90
  }
82
91
 
@@ -93,7 +102,7 @@ export class Repository<T> {
93
102
 
94
103
  /**
95
104
  * @internal
96
- * Validates an entity against the 'required' constraints defined in decorators.
105
+ * Validates an entity against the 'required' constraints defined in the model configuration.
97
106
  * @param entity The partial entity to validate.
98
107
  */
99
108
  private validate(entity: Partial<T>) {
@@ -112,13 +121,13 @@ export class Repository<T> {
112
121
  }
113
122
 
114
123
  /**
115
- * Runs lifecycle hooks of a given type for the entity.
116
- * @param entity The entity instance.
117
- * @param type The hook type (e.g., 'beforeCreate').
118
- */
124
+ * Runs lifecycle hooks of a given type for the entity.
125
+ * @param entity The entity instance.
126
+ * @param type The hook type (e.g., 'beforeCreate').
127
+ */
119
128
  private async runHooks(entity: any, type: HookType): Promise<void> {
120
129
  for (const hook of getHooks(entity, type)) {
121
- await hook();
130
+ await hook.callback(entity);
122
131
  }
123
132
  }
124
133
 
@@ -139,6 +148,21 @@ export class Repository<T> {
139
148
  return qb;
140
149
  }
141
150
 
151
+ /**
152
+ * Applies a custom scope to the query for the repository's table.
153
+ * @param name The name of the scope to apply.
154
+ * @param args Optional arguments to pass to the scope function.
155
+ * @returns A `QueryBuilder` instance with the scope applied.
156
+ * @example
157
+ * ```
158
+ * const activeUsers = await userRepository.scope('active').execute(client);
159
+ * ```
160
+ */
161
+ scope(name: string, ...args: any[]): QueryBuilder<T> {
162
+ this.logger.logDebug(`Applying scope ${name} to ${this.table}`);
163
+ return this.find().scope(name, ...args);
164
+ }
165
+
142
166
  /**
143
167
  * Finds a single record by its primary key (id).
144
168
  * @param id The ID of the record to find.
@@ -173,8 +197,8 @@ export class Repository<T> {
173
197
  }
174
198
 
175
199
  /**
176
- * Snapshot query: get record as it was at a point in time.
177
- */
200
+ * Snapshot query: get record as it was at a point in time.
201
+ */
178
202
  async asOf(
179
203
  id: number | string,
180
204
  asOfDate: Date,
@@ -190,8 +214,8 @@ export class Repository<T> {
190
214
  }
191
215
 
192
216
  /**
193
- * Get all history for a record.
194
- */
217
+ * Get all history for a record.
218
+ */
195
219
  async history(
196
220
  id: number | string,
197
221
  _client?: DBClient
@@ -204,8 +228,6 @@ export class Repository<T> {
204
228
  );
205
229
  }
206
230
 
207
-
208
-
209
231
  /**
210
232
  * Rollback a record to a previous version.
211
233
  */
@@ -237,21 +259,12 @@ export class Repository<T> {
237
259
  });
238
260
  }
239
261
 
240
-
241
262
  /**
242
263
  * Writes a versioned history row for the entity to the history table.
243
- *
244
- * This method maps entity property keys to their corresponding SQL column names
245
- * (as defined in metadata) to ensure that inserts match the schema for all supported
246
- * databases (Postgres, MySQL, SQLite).
247
- *
248
- * This function works for Postgres, MySQL, and SQLite, and uses positional parameters
249
- * (properly formatted for the target database) for safety and compatibility.
250
- *
251
- * @param entity - The entity object being versioned
252
- * @param operation - The operation performed ("insert", "update", "delete")
253
- * @param client - The database client to use for the insert
254
- * @param user - The user/system responsible for the change (default: "system")
264
+ * @param entity The entity object being versioned
265
+ * @param operation The operation performed ("insert", "update", "delete")
266
+ * @param client The database client to use for the insert
267
+ * @param user The user/system responsible for the change (default: "system")
255
268
  */
256
269
  private async writeHistory(
257
270
  entity: any,
@@ -261,11 +274,9 @@ export class Repository<T> {
261
274
  ) {
262
275
  if (!this.versioned) return;
263
276
 
264
- // Get property keys and corresponding SQL column names
265
277
  const propertyKeys = Object.keys(this.columns);
266
278
  const sqlColumnNames = propertyKeys.map((k) => this.columns[k]!.name);
267
279
 
268
- // Build historyColumns using SQL column names
269
280
  const historyColumns = [
270
281
  ...sqlColumnNames,
271
282
  "operation",
@@ -276,9 +287,7 @@ export class Repository<T> {
276
287
  "modified_at"
277
288
  ];
278
289
 
279
- // Map values from entity using property keys
280
290
  const values = propertyKeys.map((k) => entity[k]);
281
-
282
291
  const params = [
283
292
  ...values,
284
293
  operation,
@@ -288,7 +297,7 @@ export class Repository<T> {
288
297
  user || "system",
289
298
  new Date()
290
299
  ];
291
- // Database-agnostic placeholder formatting
300
+
292
301
  let placeholders: string;
293
302
  if (client.config.type === DBType.Postgres) {
294
303
  placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
@@ -332,6 +341,7 @@ export class Repository<T> {
332
341
  return result;
333
342
  });
334
343
  }
344
+
335
345
  /**
336
346
  * @internal
337
347
  * The private implementation for creating a record, executed within a transaction.
@@ -407,7 +417,6 @@ export class Repository<T> {
407
417
  options: { relations?: string[]; batchSize?: number } = {},
408
418
  ): Promise<T[]> {
409
419
  return this.client.transaction(async (txClient) => {
410
- // Prepare entity instances for hooks
411
420
  const preparedEntities = entities.map(data => {
412
421
  const instance = new (this as any).model();
413
422
  Object.assign(instance, data);
@@ -464,7 +473,6 @@ export class Repository<T> {
464
473
  );
465
474
 
466
475
  if (dbType === DBType.Postgres) {
467
- // PostgreSQL: numbered placeholders ($1, $2, ...)
468
476
  let paramIdx = 1;
469
477
  const valuePlaceholders = batch
470
478
  .map(
@@ -476,7 +484,6 @@ export class Repository<T> {
476
484
  const batchResults = await client.query<T>(query, params);
477
485
  const ids = batchResults.map((r) => (r as any).id);
478
486
 
479
- // Handle relation loading if needed
480
487
  let finalResults = batchResults;
481
488
  if (ids.length > 0 && batchResults.length === 0) {
482
489
  const queryBuilder = this.find().where(
@@ -493,7 +500,6 @@ export class Repository<T> {
493
500
  results.push(...finalResults);
494
501
 
495
502
  } else {
496
- // SQLite/MySQL: ? placeholders
497
503
  const placeholders = `(${keys.map(() => "?").join(", ")})`;
498
504
  query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
499
505
  await client.query(query, params);
@@ -642,17 +648,14 @@ export class Repository<T> {
642
648
  for (let i = 0; i < updates.length; i += batchSize) {
643
649
  const batch = updates.slice(i, i + batchSize);
644
650
  for (const update of batch) {
645
- // Find all IDs matching the where clause
646
651
  const rows = await client.query<{ id: number | string }>(
647
652
  `SELECT id FROM ${this.table} WHERE ${update.where.condition}${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`,
648
653
  update.where.params,
649
654
  );
650
655
  for (const { id } of rows) {
651
- // Fetch record before update for versioning
652
656
  const before = await this.findOne(id, {}, client);
653
657
  if (!before) continue;
654
658
 
655
- // Prepare instance for hooks
656
659
  const instance = new ((this as any).model || Object)();
657
660
  Object.assign(instance, before, update.set);
658
661
 
@@ -693,6 +696,7 @@ export class Repository<T> {
693
696
  `Bulk updated ${updates.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
694
697
  );
695
698
  }
699
+
696
700
  /**
697
701
  * Performs an "update or insert" operation based on a set of unique keys.
698
702
  * @param entity The entity to upsert.
@@ -728,6 +732,7 @@ export class Repository<T> {
728
732
  const columns = Object.keys(entity).filter((k) => this.columns[k]);
729
733
  const columnNames = columns.map((k) => this.columns[k]?.name).join(", ");
730
734
  const placeholders = columns.map(() => "?").join(", ");
735
+
731
736
  const updateClause = columns
732
737
  .filter((c) => !keys.includes(c))
733
738
  .map((c) => `${this.columns[c]?.name} = ?`).join(", ");
@@ -737,7 +742,6 @@ export class Repository<T> {
737
742
  const insertParams = columns.map((k) => (entity as any)[k]);
738
743
  let params = [...insertParams, ...updateParams];
739
744
 
740
- // Try to find the record before upsert
741
745
  let before: T | null = null;
742
746
  let isUpdate = false;
743
747
  if (this.versioned && keys.length > 0) {
@@ -751,7 +755,6 @@ export class Repository<T> {
751
755
  isUpdate = !!before;
752
756
  }
753
757
 
754
- // Prepare instance for hooks
755
758
  const instance = new ((this as any).model || Object)();
756
759
  Object.assign(instance, before || {}, entity);
757
760
 
@@ -767,7 +770,7 @@ export class Repository<T> {
767
770
  query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON CONFLICT(${keys.map((k) => this.columns[k]!.name).join(", ")}) DO UPDATE SET ${updateClause}`;
768
771
  } else if (dbType === DBType.MySQL) {
769
772
  query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updateClause}`;
770
- } else { // PostgreSQL
773
+ } else {
771
774
  const pgUpdateClause = columns
772
775
  .filter((c) => !keys.includes(c))
773
776
  .map((c) => `${this.columns[c]?.name} = EXCLUDED.${this.columns[c]?.name}`).join(", ");
@@ -799,13 +802,6 @@ export class Repository<T> {
799
802
  await this.runHooks(result, "afterSave");
800
803
  }
801
804
 
802
- if (this.cache) {
803
- await this.cache.invalidatePattern(`find:${this.table}:*`);
804
- if (this.cache.getStrategy() === "write-through") {
805
- await this.cache.set(`findOne:${this.table}:${id}`, [result], 60);
806
- }
807
- }
808
-
809
805
  if (this.versioned) {
810
806
  await this.writeHistory(
811
807
  { ...result, version: before ? ((before as any).version ? (before as any).version + 1 : 1) : 1 },
@@ -814,11 +810,19 @@ export class Repository<T> {
814
810
  );
815
811
  }
816
812
 
813
+ if (this.cache) {
814
+ await this.cache.invalidatePattern(`find:${this.table}:*`);
815
+ if (this.cache.getStrategy() === "write-through") {
816
+ await this.cache.set(`findOne:${this.table}:${id}`, [result], 60);
817
+ }
818
+ }
819
+
817
820
  this.logger.logDebug(
818
821
  `Upserted ${this.table} with ID ${id} in ${(performance.now() - start).toFixed(2)}ms`,
819
822
  );
820
823
  return result;
821
824
  }
825
+
822
826
  /**
823
827
  * Deletes a record by its ID. Performs a soft delete if enabled on the model.
824
828
  * @param id The ID of the record to delete.
@@ -929,6 +933,7 @@ export class Repository<T> {
929
933
  `Bulk deleted ${ids.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
930
934
  );
931
935
  }
936
+
932
937
  /**
933
938
  * Recovers a soft-deleted record by its ID.
934
939
  * Throws an error if soft delete is not enabled on the model.
@@ -1014,7 +1019,7 @@ export class Repository<T> {
1014
1019
  "RELATION_ERROR",
1015
1020
  );
1016
1021
 
1017
- const relatedTable = Reflect.getMetadata(ModelKey, rel.targetModel());
1022
+ const relatedTable = MetadataStorage.getTableName(rel.targetModel());
1018
1023
  if (
1019
1024
  rel.type === RelationType.OneToOne ||
1020
1025
  rel.type === RelationType.ManyToOne
@@ -1040,4 +1045,4 @@ export class Repository<T> {
1040
1045
  );
1041
1046
  }
1042
1047
  }
1043
- }
1048
+ }
package/types.ts CHANGED
@@ -42,6 +42,7 @@ export enum DataTypes {
42
42
  JSON, // Maps to JSON, JSONB, or TEXT
43
43
  UUID, // Maps to UUID or VARCHAR(36)
44
44
  BLOB, // Maps to BYTEA or BLOB
45
+
45
46
  }
46
47
 
47
48
  export interface DBConfig {