stabilize-orm 1.0.5

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.
File without changes
@@ -0,0 +1,11 @@
1
+ import { Model, Column, Required } from "../../src";
2
+
3
+ @Model("roles")
4
+ export class Role {
5
+ @Column("id", "INTEGER")
6
+ id?: number;
7
+
8
+ @Column("name", "TEXT")
9
+ @Required()
10
+ name?: string;
11
+ }
@@ -0,0 +1,22 @@
1
+ import { Model, Column, Required, SoftDelete } from "../../src";
2
+
3
+ @Model("users")
4
+ export class User {
5
+ @Column("id", "INTEGER")
6
+ id?: number;
7
+
8
+ @Column("name", "TEXT")
9
+ @Required()
10
+ name?: string;
11
+
12
+ @Column("email", "TEXT")
13
+ @Required()
14
+ email?: string;
15
+
16
+ @Column("active", "BOOLEAN")
17
+ active?: boolean;
18
+
19
+ @Column("deletedAt", "TEXT")
20
+ @SoftDelete()
21
+ deletedAt?: string;
22
+ }
@@ -0,0 +1,29 @@
1
+ import { Stabilize } from "../../src";
2
+ import { Role } from "../models/Role";
3
+
4
+ export const dependencies = ["20251013_initial_seed"];
5
+
6
+ export async function seed(orm: Stabilize) {
7
+ const repo = orm.getRepository(Role);
8
+ await repo.bulkCreate([{ name: "Admin" }, { name: "User" }], {
9
+ batchSize: 100,
10
+ });
11
+
12
+ await orm["client"].query(
13
+ `INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`,
14
+ ["20251013_additional_seed", new Date().toISOString()],
15
+ );
16
+ }
17
+
18
+ export async function rollback(orm: Stabilize) {
19
+ const repo = orm.getRepository(Role);
20
+ const entities = await repo.find().execute(orm["client"]);
21
+ await repo.bulkDelete(
22
+ entities.map((e) => e.id!),
23
+ { batchSize: 100 },
24
+ );
25
+
26
+ await orm["client"].query(`DELETE FROM seed_history WHERE name = ?`, [
27
+ "20251013_additional_seed",
28
+ ]);
29
+ }
@@ -0,0 +1,33 @@
1
+ import { Stabilize } from "../../src";
2
+ import { User } from "../models/User";
3
+
4
+ export const dependencies = [];
5
+
6
+ export async function seed(orm: Stabilize) {
7
+ const repo = orm.getRepository(User);
8
+ await repo.bulkCreate(
9
+ [
10
+ { name: "Alice", email: "alice@example.com", active: true },
11
+ { name: "Bob", email: "bob@example.com", active: true },
12
+ ],
13
+ { batchSize: 100 },
14
+ );
15
+
16
+ await orm["client"].query(
17
+ `INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`,
18
+ ["20251013_initial_seed", new Date().toISOString()],
19
+ );
20
+ }
21
+
22
+ export async function rollback(orm: Stabilize) {
23
+ const repo = orm.getRepository(User);
24
+ const entities = await repo.find().execute(orm["client"]);
25
+ await repo.bulkDelete(
26
+ entities.map((e) => e.id!),
27
+ { batchSize: 100 },
28
+ );
29
+
30
+ await orm["client"].query(`DELETE FROM seed_history WHERE name = ?`, [
31
+ "20251013_initial_seed",
32
+ ]);
33
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "stabilize-orm",
3
+ "version": "1.0.5",
4
+ "description": "A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "stabilize": "./dist/cli/stabilize-cli.js"
9
+ },
10
+ "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",
12
+
13
+ "prepublishOnly": "bun run build && bun run test",
14
+ "format": "bunx prettier --write .",
15
+ "lint": "eslint src tests cli examples",
16
+ "cli": "bun run cli/stabilize-cli.ts"
17
+ },
18
+ "keywords": [
19
+ "orm",
20
+ "bun",
21
+ "sqlite",
22
+ "mysql",
23
+ "postgresql",
24
+ "redis",
25
+ "typescript",
26
+ "database"
27
+ ],
28
+ "author": "th3b0tk1ill3r <lwazicd@icloud.com>",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "ioredis": "^5.4.1",
32
+ "reflect-metadata": "^0.2.2",
33
+ "commander": "^12.1.0",
34
+ "glob": "^11.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "vitest": "^2.1.3",
38
+ "@vitest/coverage-v8": "^2.1.3",
39
+ "typescript": "^5.6.3",
40
+ "prettier": "^3.3.3",
41
+ "eslint": "^9.12.0",
42
+ "@typescript-eslint/parser": "^8.7.0",
43
+ "@typescript-eslint/eslint-plugin": "^8.7.0"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/ElectronSz/stabilize-orm.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/ElectronSz/stabilize-orm/issues"
51
+ },
52
+ "homepage": "https://github.com/ElectronSz/stabilize-orm#readme",
53
+ "engines": {
54
+ "bun": ">=1.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "bun": ">=1.0.0"
58
+ }
59
+ }
package/src/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ElectronSz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/src/cache.ts ADDED
@@ -0,0 +1,110 @@
1
+ import Redis from "ioredis";
2
+ import { type CacheConfig, type CacheStats } from "./types";
3
+ import { ConsoleLogger, type Logger } from "./logger";
4
+
5
+ export class Cache {
6
+ private redis: Redis | null = null;
7
+ private ttl: number;
8
+ private prefix: string;
9
+ private strategy: "cache-aside" | "write-through";
10
+ private logger: Logger;
11
+ private hits: number = 0;
12
+ private misses: number = 0;
13
+
14
+ constructor(config: CacheConfig, logger: Logger = new ConsoleLogger()) {
15
+ this.ttl = config.ttl;
16
+ this.prefix = config.cachePrefix || "cache:";
17
+ this.strategy = config.strategy || "cache-aside";
18
+ this.logger = logger;
19
+
20
+ if (config.enabled && config.redisUrl) {
21
+ this.redis = new Redis(config.redisUrl, { lazyConnect: true });
22
+ this.redis.on("error", (error) => this.logger.logError(error));
23
+ }
24
+ }
25
+
26
+ getStrategy() {
27
+ return this.strategy;
28
+ }
29
+
30
+ async get<T>(key: string): Promise<T | null> {
31
+ if (!this.redis) return null;
32
+
33
+ try {
34
+ const data = await this.redis.get(this.prefix + key);
35
+ if (data) {
36
+ this.hits++;
37
+ this.logger.logDebug(`Cache hit for key: ${key}`);
38
+ return JSON.parse(data) as T;
39
+ }
40
+ this.misses++;
41
+ this.logger.logDebug(`Cache miss for key: ${key}`);
42
+ return null;
43
+ } catch (error) {
44
+ this.logger.logError(error as Error);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ async set<T>(key: string, value: T, ttl: number = this.ttl): Promise<void> {
50
+ if (!this.redis) return;
51
+
52
+ try {
53
+ await this.redis.set(this.prefix + key, JSON.stringify(value), "EX", ttl);
54
+ this.logger.logDebug(`Cache set for key: ${key}`);
55
+ } catch (error) {
56
+ this.logger.logError(error as Error);
57
+ }
58
+ }
59
+
60
+ async invalidate(keys: string[]): Promise<void> {
61
+ if (!this.redis) return;
62
+
63
+ try {
64
+ const pipeline = this.redis.pipeline();
65
+ for (const key of keys) {
66
+ pipeline.del(this.prefix + key);
67
+ this.logger.logDebug(`Cache invalidated for key: ${key}`);
68
+ }
69
+ await pipeline.exec();
70
+ } catch (error) {
71
+ this.logger.logError(error as Error);
72
+ }
73
+ }
74
+
75
+ async invalidatePattern(pattern: string): Promise<void> {
76
+ if (!this.redis) return;
77
+
78
+ try {
79
+ const keys = await this.redis.keys(this.prefix + pattern);
80
+ if (keys.length > 0) {
81
+ const pipeline = this.redis.pipeline();
82
+ for (const key of keys) {
83
+ pipeline.del(key);
84
+ this.logger.logDebug(`Cache invalidated for pattern: ${pattern}`);
85
+ }
86
+ await pipeline.exec();
87
+ }
88
+ } catch (error) {
89
+ this.logger.logError(error as Error);
90
+ }
91
+ }
92
+
93
+ async getStats(): Promise<CacheStats> {
94
+ if (!this.redis) return { hits: 0, misses: 0, keys: 0 };
95
+ try {
96
+ const keys = await this.redis.keys(this.prefix + "*");
97
+ return { hits: this.hits, misses: this.misses, keys: keys.length };
98
+ } catch (error) {
99
+ this.logger.logError(error as Error);
100
+ return { hits: this.hits, misses: this.misses, keys: 0 };
101
+ }
102
+ }
103
+
104
+ async disconnect(): Promise<void> {
105
+ if (this.redis) {
106
+ await this.redis.quit();
107
+ this.logger.logInfo("Redis connection closed");
108
+ }
109
+ }
110
+ }
package/src/client.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { sql, SQL } from "bun";
2
+ import { Database, Statement } from "bun:sqlite";
3
+ import {
4
+ type DBConfig,
5
+ StabilizeError,
6
+ type PoolMetrics,
7
+ DBType, // Added DBType for type checking
8
+ type LoggerConfig, // Kept, but not used in logic
9
+ } from "./types";
10
+ import { type Logger, ConsoleLogger } from "./logger";
11
+
12
+ // Helper to determine if we are in Bun SQLite mode based on config
13
+ function isSQLiteConfig(config: DBConfig): boolean {
14
+ return (
15
+ config.type === DBType.SQLite || config.connectionString.includes("sqlite")
16
+ );
17
+ }
18
+
19
+ export class DBClient {
20
+ // Renamed to 'client' for clarity; stores the actual connection (Bun.Database or external driver)
21
+ // We use 'any' for the SQL client instance since Bun's SQL client is complex (callable function with methods)
22
+ private client: Database | any | null = null;
23
+ private logger: Logger;
24
+ private retryAttempts: number;
25
+ private retryDelay: number;
26
+ private maxJitter: number;
27
+ // Map stores Bun SQLite Statement objects when in SQLite mode
28
+ private preparedStatements: Map<string, Statement> = new Map();
29
+ private config: DBConfig;
30
+
31
+ constructor(config: DBConfig, logger: Logger = new ConsoleLogger()) {
32
+ this.config = config;
33
+ this.logger = logger;
34
+ this.retryAttempts = config.retryAttempts || 3;
35
+ this.retryDelay = config.retryDelay || 1000;
36
+ this.maxJitter = config.maxJitter || 100;
37
+
38
+ this.initializeClient(config);
39
+ }
40
+
41
+ // Initializes the connection based on config
42
+ private initializeClient(config: DBConfig) {
43
+ if (isSQLiteConfig(config)) {
44
+ // Use Bun's native SQLite client, which is synchronous to construct
45
+ try {
46
+ // new Database(path, { create: true }) is the standard Bun method
47
+ this.client = new Database(config.connectionString, { create: true });
48
+ this.logger.logDebug(
49
+ `Initialized Bun SQLite client for: ${config.connectionString}`,
50
+ );
51
+ } catch (e) {
52
+ this.logger.logError(e as Error);
53
+ throw new StabilizeError(
54
+ `Failed to initialize SQLite database: ${(e as Error).message}`,
55
+ "INIT_ERROR",
56
+ );
57
+ }
58
+ } else {
59
+ // For other DB types (Postgres/MySQL), initialize a Bun SQL client instance.
60
+ // This is necessary because the bare `sql` tag is for the default connection,
61
+ // and we need an instance with specific connection settings, using `SQL` as the constructor.
62
+ this.client = new SQL(config.connectionString);
63
+ this.logger.logDebug(
64
+ `Initialized Bun SQL client for: ${config.connectionString}`,
65
+ );
66
+ }
67
+ }
68
+
69
+ private getJitter() {
70
+ return Math.random() * this.maxJitter;
71
+ }
72
+
73
+ // Pool metrics are largely irrelevant for single-connection Bun SQLite
74
+ getPoolMetrics(): PoolMetrics {
75
+ return {
76
+ activeConnections: 0,
77
+ idleConnections: 0,
78
+ totalConnections: this.client instanceof Database ? 1 : 0,
79
+ };
80
+ }
81
+
82
+ async switchConnection(config: DBConfig) {
83
+ await this.close();
84
+ this.config = config;
85
+ this.preparedStatements.clear();
86
+ this.retryAttempts = config.retryAttempts || 3;
87
+ this.retryDelay = config.retryDelay || 1000;
88
+ this.maxJitter = config.maxJitter || 100;
89
+ this.initializeClient(config);
90
+ }
91
+
92
+ async query<T>(query: string, params: any[] = []): Promise<T[]> {
93
+ const start = Date.now();
94
+ this.logger.logQuery(query, params);
95
+ this.logger.logMetrics(this.getPoolMetrics());
96
+
97
+ let stmt: Statement | undefined;
98
+
99
+ // Check for Bun SQLite client
100
+ if (this.client instanceof Database) {
101
+ const stmtKey = query;
102
+ // Use prepared statement caching for SQLite
103
+ if (!this.preparedStatements.has(stmtKey)) {
104
+ // Use this.client (the Database instance) to prepare the statement
105
+ this.preparedStatements.set(stmtKey, this.client.prepare(query));
106
+ }
107
+ stmt = this.preparedStatements.get(stmtKey);
108
+ }
109
+
110
+ for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
111
+ try {
112
+ let result: T[];
113
+
114
+ if (stmt) {
115
+ // Bun SQLite Statement objects use .all() for fetching results
116
+ result = stmt.all(...params) as T[];
117
+ } else if (this.client) {
118
+ // Path for non-SQLite Bun SQL clients (Postgres/MySQL)
119
+ // Bun SQL clients do not expose a standard .query() method.
120
+ // We must use the 'unsafe' helper to execute a raw string query with positional parameters.
121
+ result = (await (this.client as any).unsafe(query, params)) as T[];
122
+ } else {
123
+ throw new StabilizeError(
124
+ "Database client is not initialized or does not support query execution.",
125
+ "INIT_ERROR",
126
+ );
127
+ }
128
+
129
+ const executionTime = Date.now() - start;
130
+ this.logger.logQuery(query, params, executionTime);
131
+ return result;
132
+ } catch (error) {
133
+ this.logger.logError(error as Error);
134
+ if (attempt === this.retryAttempts) {
135
+ throw new StabilizeError(
136
+ `Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`,
137
+ "QUERY_ERROR",
138
+ );
139
+ }
140
+ // Exponential backoff with jitter
141
+ await new Promise((resolve) =>
142
+ setTimeout(
143
+ resolve,
144
+ this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter(),
145
+ ),
146
+ );
147
+ }
148
+ }
149
+ throw new StabilizeError("Query failed: no attempts made", "QUERY_ERROR");
150
+ }
151
+
152
+ async transaction<T>(callback: () => Promise<T>): Promise<T> {
153
+ // Use native Bun SQLite transaction wrapper for safer, faster transactions
154
+ if (this.client instanceof Database) {
155
+ const start = Date.now();
156
+ this.logger.logDebug("Starting native SQLite transaction");
157
+
158
+ // Bun's .transaction() automatically handles BEGIN/COMMIT/ROLLBACK
159
+ const tx = this.client.transaction(async () => {
160
+ return callback();
161
+ });
162
+
163
+ try {
164
+ const result = await tx();
165
+ this.logger.logDebug(
166
+ `Native transaction committed in ${Date.now() - start}ms`,
167
+ );
168
+ return result;
169
+ } catch (error) {
170
+ this.logger.logError(error as Error);
171
+ // The transaction automatically rolls back on error
172
+ throw new StabilizeError(
173
+ `Native transaction failed: ${(error as Error).message}`,
174
+ "TX_ERROR",
175
+ );
176
+ }
177
+ }
178
+
179
+ // Fallback to manual transaction with retry logic for non-SQLite
180
+ const start = Date.now();
181
+ this.logger.logDebug("Starting manual transaction (non-SQLite)");
182
+ for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
183
+ try {
184
+ // Use this.query, which now correctly handles execution via .unsafe() for non-SQLite
185
+ await this.query("BEGIN", []);
186
+ const result = await callback();
187
+ await this.query("COMMIT", []);
188
+ this.logger.logDebug(
189
+ `Manual transaction committed in ${Date.now() - start}ms`,
190
+ );
191
+ return result;
192
+ } catch (error) {
193
+ // Attempt rollback, but ignore errors if rollback fails
194
+ await this.query("ROLLBACK", []).catch(() => {
195
+ this.logger.logDebug("Rollback failed. Connection may be invalid.");
196
+ });
197
+ this.logger.logError(error as Error);
198
+ if (attempt === this.retryAttempts) {
199
+ throw new StabilizeError(
200
+ `Manual transaction failed after ${this.retryAttempts} attempts: ${(error as Error).message}`,
201
+ "TX_ERROR",
202
+ );
203
+ }
204
+ // Exponential backoff with jitter
205
+ await new Promise((resolve) =>
206
+ setTimeout(
207
+ resolve,
208
+ this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter(),
209
+ ),
210
+ );
211
+ }
212
+ }
213
+ throw new StabilizeError(
214
+ "Transaction failed: no attempts made",
215
+ "TX_ERROR",
216
+ );
217
+ }
218
+
219
+ async savepoint<T>(name: string, callback: () => Promise<T>): Promise<T> {
220
+ const start = Date.now();
221
+ this.logger.logDebug(`Starting savepoint ${name}`);
222
+ await this.query(`SAVEPOINT ${name}`, []);
223
+ try {
224
+ const result = await callback();
225
+ await this.query(`RELEASE SAVEPOINT ${name}`, []);
226
+ this.logger.logDebug(
227
+ `Savepoint ${name} released in ${Date.now() - start}ms`,
228
+ );
229
+ return result;
230
+ } catch (error) {
231
+ // Attempt rollback, but ignore errors if rollback fails
232
+ await this.query(`ROLLBACK TO SAVEPOINT ${name}`, []).catch(() => {
233
+ this.logger.logDebug(
234
+ `Rollback to savepoint ${name} failed. Connection may be invalid.`,
235
+ );
236
+ });
237
+ this.logger.logError(error as Error);
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ async close() {
243
+ this.preparedStatements.clear();
244
+ // Check if the client is a Bun Database instance before closing (Bun SQLite close is sync)
245
+ if (this.client instanceof Database) {
246
+ this.client.close();
247
+ this.client = null;
248
+ } else if (
249
+ this.client &&
250
+ typeof (this.client as any).close === "function"
251
+ ) {
252
+ // Assume external/Bun SQL driver has an async close method
253
+ await (this.client as any).close();
254
+ this.client = null;
255
+ }
256
+ this.logger.logInfo("Database connection closed");
257
+ }
258
+ }
@@ -0,0 +1,99 @@
1
+ import "reflect-metadata";
2
+ import { RelationType } from "./types";
3
+
4
+ export const ModelKey = Symbol("model");
5
+ export const ColumnKey = Symbol("column");
6
+ export const ValidatorKey = Symbol("validator");
7
+ export const RelationKey = Symbol("relation");
8
+ export const SoftDeleteKey = Symbol("softDelete");
9
+
10
+ export function Model(tableName: string) {
11
+ return function (constructor: Function) {
12
+ Reflect.defineMetadata(ModelKey, tableName, constructor);
13
+ };
14
+ }
15
+
16
+ export function Column(name: string, type: string) {
17
+ return function (target: any, propertyKey: string) {
18
+ const columns = Reflect.getMetadata(ColumnKey, target) || {};
19
+ columns[propertyKey] = { name, type };
20
+ Reflect.defineMetadata(ColumnKey, columns, target);
21
+ };
22
+ }
23
+
24
+ export function Required() {
25
+ return function (target: any, propertyKey: string) {
26
+ const validators = Reflect.getMetadata(ValidatorKey, target) || {};
27
+ validators[propertyKey] = [...(validators[propertyKey] || []), "required"];
28
+ Reflect.defineMetadata(ValidatorKey, validators, target);
29
+ };
30
+ }
31
+
32
+ export function Unique() {
33
+ return function (target: any, propertyKey: string) {
34
+ const validators = Reflect.getMetadata(ValidatorKey, target) || {};
35
+ validators[propertyKey] = [...(validators[propertyKey] || []), "unique"];
36
+ Reflect.defineMetadata(ValidatorKey, validators, target);
37
+ };
38
+ }
39
+
40
+ export function SoftDelete() {
41
+ return function (target: any, propertyKey: string) {
42
+ Reflect.defineMetadata(SoftDeleteKey, propertyKey, target);
43
+ };
44
+ }
45
+
46
+ export function OneToOne(model: () => any, foreignKey: string) {
47
+ return function (target: any, propertyKey: string) {
48
+ const relations = Reflect.getMetadata(RelationKey, target) || {};
49
+ relations[propertyKey] = {
50
+ type: RelationType.OneToOne,
51
+ targetModel: model,
52
+ foreignKey,
53
+ };
54
+ Reflect.defineMetadata(RelationKey, relations, target);
55
+ };
56
+ }
57
+
58
+ export function ManyToOne(model: () => any, foreignKey: string) {
59
+ return function (target: any, propertyKey: string) {
60
+ const relations = Reflect.getMetadata(RelationKey, target) || {};
61
+ relations[propertyKey] = {
62
+ type: RelationType.ManyToOne,
63
+ targetModel: model,
64
+ foreignKey,
65
+ };
66
+ Reflect.defineMetadata(RelationKey, relations, target);
67
+ };
68
+ }
69
+
70
+ export function OneToMany(model: () => any, inverseKey: string) {
71
+ return function (target: any, propertyKey: string) {
72
+ const relations = Reflect.getMetadata(RelationKey, target) || {};
73
+ relations[propertyKey] = {
74
+ type: RelationType.OneToMany,
75
+ targetModel: model,
76
+ inverseKey,
77
+ };
78
+ Reflect.defineMetadata(RelationKey, relations, target);
79
+ };
80
+ }
81
+
82
+ export function ManyToMany(
83
+ model: () => any,
84
+ joinTable: string,
85
+ foreignKey: string,
86
+ inverseKey: string,
87
+ ) {
88
+ return function (target: any, propertyKey: string) {
89
+ const relations = Reflect.getMetadata(RelationKey, target) || {};
90
+ relations[propertyKey] = {
91
+ type: RelationType.ManyToMany,
92
+ targetModel: model,
93
+ joinTable,
94
+ foreignKey,
95
+ inverseKey,
96
+ };
97
+ Reflect.defineMetadata(RelationKey, relations, target);
98
+ };
99
+ }