@bunnykit/orm 0.1.17 → 0.1.19
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/dist/src/connection/Connection.d.ts +2 -0
- package/dist/src/connection/Connection.js +7 -0
- package/dist/src/connection/ConnectionManager.d.ts +3 -1
- package/dist/src/connection/ConnectionManager.js +42 -17
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/model/IdentityMap.d.ts +8 -0
- package/dist/src/model/IdentityMap.js +28 -0
- package/dist/src/model/Model.d.ts +1 -0
- package/dist/src/model/Model.js +11 -0
- package/dist/src/query/Builder.js +19 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ export declare class Connection {
|
|
|
8
8
|
private config;
|
|
9
9
|
private schema?;
|
|
10
10
|
private ownsDriver;
|
|
11
|
+
private transactionDepth;
|
|
11
12
|
constructor(config: ConnectionConfig, options?: {
|
|
12
13
|
driver?: SQL;
|
|
13
14
|
schema?: string;
|
|
@@ -25,6 +26,7 @@ export declare class Connection {
|
|
|
25
26
|
beginTransaction(): Promise<void>;
|
|
26
27
|
commit(): Promise<void>;
|
|
27
28
|
rollback(): Promise<void>;
|
|
29
|
+
isInTransaction(): boolean;
|
|
28
30
|
transaction<T>(callback: (connection: Connection) => T | Promise<T>): Promise<T>;
|
|
29
31
|
withTenant<T>(tenantId: string, callback: (connection: Connection) => T | Promise<T>, setting?: string): Promise<T>;
|
|
30
32
|
withSearchPath<T>(schema: string, callback: (connection: Connection) => T | Promise<T>): Promise<T>;
|
|
@@ -9,6 +9,7 @@ export class Connection {
|
|
|
9
9
|
config;
|
|
10
10
|
schema;
|
|
11
11
|
ownsDriver;
|
|
12
|
+
transactionDepth = 0;
|
|
12
13
|
constructor(config, options = {}) {
|
|
13
14
|
this.config = config;
|
|
14
15
|
this.schema = options.schema || ("schema" in config ? config.schema : undefined);
|
|
@@ -83,12 +84,18 @@ export class Connection {
|
|
|
83
84
|
}
|
|
84
85
|
async beginTransaction() {
|
|
85
86
|
await this.driver.unsafe("BEGIN");
|
|
87
|
+
this.transactionDepth++;
|
|
86
88
|
}
|
|
87
89
|
async commit() {
|
|
88
90
|
await this.driver.unsafe("COMMIT");
|
|
91
|
+
this.transactionDepth = Math.max(0, this.transactionDepth - 1);
|
|
89
92
|
}
|
|
90
93
|
async rollback() {
|
|
91
94
|
await this.driver.unsafe("ROLLBACK");
|
|
95
|
+
this.transactionDepth = Math.max(0, this.transactionDepth - 1);
|
|
96
|
+
}
|
|
97
|
+
isInTransaction() {
|
|
98
|
+
return this.transactionDepth > 0;
|
|
92
99
|
}
|
|
93
100
|
async transaction(callback) {
|
|
94
101
|
return await this.driver.begin(async (sql) => {
|
|
@@ -33,16 +33,18 @@ export declare class ConnectionManager {
|
|
|
33
33
|
private static poolConfigs;
|
|
34
34
|
private static tenantResolver?;
|
|
35
35
|
private static tenantCache;
|
|
36
|
+
private static waiters;
|
|
36
37
|
static setDefault(connection: Connection): void;
|
|
37
38
|
static getDefault(): Connection | undefined;
|
|
38
39
|
static setPoolConfig(name: string, config: PoolConfig): void;
|
|
39
40
|
static getPoolConfig(name: string): PoolConfig | undefined;
|
|
40
41
|
private static getPooledConnection;
|
|
42
|
+
private static removeWaiter;
|
|
41
43
|
private static releasePooledConnection;
|
|
42
44
|
static add(name: string, connection: Connection | ConnectionConfig): Connection;
|
|
43
45
|
static get(name: string): Connection | undefined;
|
|
44
46
|
static getPooled(name: string, config?: ConnectionConfig): Promise<Connection>;
|
|
45
|
-
static release(name: string, connection: Connection): void
|
|
47
|
+
static release(name: string, connection: Connection): Promise<void>;
|
|
46
48
|
static require(name: string): Connection;
|
|
47
49
|
static setTenantResolver(resolver: TenantResolver): void;
|
|
48
50
|
static resolveTenant(tenantId: string): Promise<ActiveTenantContext>;
|
|
@@ -6,6 +6,7 @@ export class ConnectionManager {
|
|
|
6
6
|
static poolConfigs = new Map();
|
|
7
7
|
static tenantResolver;
|
|
8
8
|
static tenantCache = new Map();
|
|
9
|
+
static waiters = new Map();
|
|
9
10
|
static setDefault(connection) {
|
|
10
11
|
this.defaultConnection = connection;
|
|
11
12
|
}
|
|
@@ -34,7 +35,7 @@ export class ConnectionManager {
|
|
|
34
35
|
const pooled = pool[idx];
|
|
35
36
|
pool.splice(idx, 1);
|
|
36
37
|
try {
|
|
37
|
-
pooled.connection.query("SELECT 1")
|
|
38
|
+
await pooled.connection.query("SELECT 1");
|
|
38
39
|
pooled.inUse = true;
|
|
39
40
|
return pooled.connection;
|
|
40
41
|
}
|
|
@@ -48,30 +49,54 @@ export class ConnectionManager {
|
|
|
48
49
|
return connection;
|
|
49
50
|
}
|
|
50
51
|
return new Promise((resolve, reject) => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
if (available) {
|
|
54
|
-
clearInterval(checkInterval);
|
|
55
|
-
available.inUse = true;
|
|
56
|
-
available.lastUsed = Date.now();
|
|
57
|
-
resolve(available.connection);
|
|
58
|
-
}
|
|
59
|
-
}, 50);
|
|
60
|
-
setTimeout(() => {
|
|
61
|
-
clearInterval(checkInterval);
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
this.removeWaiter(name, waiter);
|
|
62
54
|
reject(new Error(`Connection pool exhausted for "${name}"`));
|
|
63
55
|
}, 30000);
|
|
56
|
+
const waiter = { resolve, reject, timer };
|
|
57
|
+
let poolWaiters = this.waiters.get(name);
|
|
58
|
+
if (!poolWaiters) {
|
|
59
|
+
poolWaiters = [];
|
|
60
|
+
this.waiters.set(name, poolWaiters);
|
|
61
|
+
}
|
|
62
|
+
poolWaiters.push(waiter);
|
|
64
63
|
});
|
|
65
64
|
}
|
|
66
|
-
static
|
|
65
|
+
static removeWaiter(name, waiter) {
|
|
66
|
+
const poolWaiters = this.waiters.get(name);
|
|
67
|
+
if (!poolWaiters)
|
|
68
|
+
return;
|
|
69
|
+
const idx = poolWaiters.indexOf(waiter);
|
|
70
|
+
if (idx !== -1)
|
|
71
|
+
poolWaiters.splice(idx, 1);
|
|
72
|
+
}
|
|
73
|
+
static async releasePooledConnection(name, connection) {
|
|
67
74
|
const pool = this.pools.get(name);
|
|
68
75
|
if (!pool)
|
|
69
76
|
return;
|
|
70
77
|
const pooled = pool.find((p) => p.connection === connection);
|
|
71
|
-
if (pooled)
|
|
72
|
-
|
|
78
|
+
if (!pooled)
|
|
79
|
+
return;
|
|
80
|
+
// Safety: roll back any leaked transaction before returning to pool
|
|
81
|
+
if (pooled.connection.isInTransaction()) {
|
|
82
|
+
try {
|
|
83
|
+
await pooled.connection.rollback();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Connection may already be dead; ignore rollback errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Hand off directly to a waiter if one exists
|
|
90
|
+
const poolWaiters = this.waiters.get(name);
|
|
91
|
+
if (poolWaiters && poolWaiters.length > 0) {
|
|
92
|
+
const waiter = poolWaiters.shift();
|
|
93
|
+
clearTimeout(waiter.timer);
|
|
73
94
|
pooled.lastUsed = Date.now();
|
|
95
|
+
waiter.resolve(connection);
|
|
96
|
+
return;
|
|
74
97
|
}
|
|
98
|
+
pooled.inUse = false;
|
|
99
|
+
pooled.lastUsed = Date.now();
|
|
75
100
|
}
|
|
76
101
|
static add(name, connection) {
|
|
77
102
|
const resolved = connection instanceof Connection ? connection : new Connection(connection);
|
|
@@ -90,8 +115,8 @@ export class ConnectionManager {
|
|
|
90
115
|
return existing;
|
|
91
116
|
throw new Error(`No connection registered for "${name}". Use add() first or provide config.`);
|
|
92
117
|
}
|
|
93
|
-
static release(name, connection) {
|
|
94
|
-
this.releasePooledConnection(name, connection);
|
|
118
|
+
static async release(name, connection) {
|
|
119
|
+
await this.releasePooledConnection(name, connection);
|
|
95
120
|
}
|
|
96
121
|
static require(name) {
|
|
97
122
|
const connection = this.get(name);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
|
|
|
20
20
|
export { MorphMap } from "./model/MorphMap.js";
|
|
21
21
|
export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
|
|
22
22
|
export { BelongsToMany } from "./model/BelongsToMany.js";
|
|
23
|
+
export { IdentityMap } from "./model/IdentityMap.js";
|
|
23
24
|
export { Migration } from "./migration/Migration.js";
|
|
24
25
|
export { Migrator } from "./migration/Migrator.js";
|
|
25
26
|
export type { MigrationEvent, MigrationEventListener, MigrationEventPayload } from "./migration/Migrator.js";
|
package/dist/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { ObserverRegistry } from "./model/Observer.js";
|
|
|
15
15
|
export { MorphMap } from "./model/MorphMap.js";
|
|
16
16
|
export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
|
|
17
17
|
export { BelongsToMany } from "./model/BelongsToMany.js";
|
|
18
|
+
export { IdentityMap } from "./model/IdentityMap.js";
|
|
18
19
|
export { Migration } from "./migration/Migration.js";
|
|
19
20
|
export { Migrator } from "./migration/Migrator.js";
|
|
20
21
|
export { MigrationCreator } from "./migration/MigrationCreator.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Model } from "./Model.js";
|
|
2
|
+
export declare class IdentityMap {
|
|
3
|
+
static current(): Map<string, Model> | undefined;
|
|
4
|
+
static run<T>(callback: () => T | Promise<T>): Promise<T>;
|
|
5
|
+
static get(table: string, key: string | number): Model | undefined;
|
|
6
|
+
static set(table: string, key: string | number, model: Model): void;
|
|
7
|
+
static clear(): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const storage = new AsyncLocalStorage();
|
|
3
|
+
export class IdentityMap {
|
|
4
|
+
static current() {
|
|
5
|
+
return storage.getStore();
|
|
6
|
+
}
|
|
7
|
+
static async run(callback) {
|
|
8
|
+
return await storage.run(new Map(), callback);
|
|
9
|
+
}
|
|
10
|
+
static get(table, key) {
|
|
11
|
+
const map = this.current();
|
|
12
|
+
if (!map)
|
|
13
|
+
return undefined;
|
|
14
|
+
return map.get(`${table}:${String(key)}`);
|
|
15
|
+
}
|
|
16
|
+
static set(table, key, model) {
|
|
17
|
+
const map = this.current();
|
|
18
|
+
if (!map)
|
|
19
|
+
return;
|
|
20
|
+
map.set(`${table}:${String(key)}`, model);
|
|
21
|
+
}
|
|
22
|
+
static clear() {
|
|
23
|
+
const map = this.current();
|
|
24
|
+
if (!map)
|
|
25
|
+
return;
|
|
26
|
+
map.clear();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -115,6 +115,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
115
115
|
static getTable(): string;
|
|
116
116
|
static getConnection(): Connection;
|
|
117
117
|
static setConnection(connection: Connection): void;
|
|
118
|
+
static useIdentityMap<T>(callback: () => T | Promise<T>): Promise<T>;
|
|
118
119
|
static on<M extends ModelConstructor>(this: M, connection: string | Connection): Builder<InstanceType<M>>;
|
|
119
120
|
static forTenant<M extends ModelConstructor>(this: M, tenantId: string): Builder<InstanceType<M>>;
|
|
120
121
|
static query<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
package/dist/src/model/Model.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Schema } from "../schema/Schema.js";
|
|
|
7
7
|
import { ModelNotFoundError } from "./ModelNotFoundError.js";
|
|
8
8
|
import { ConnectionManager } from "../connection/ConnectionManager.js";
|
|
9
9
|
import { TenantContext } from "../connection/TenantContext.js";
|
|
10
|
+
import { IdentityMap } from "./IdentityMap.js";
|
|
10
11
|
const globalScopes = new WeakMap();
|
|
11
12
|
function getGlobalScopes(model) {
|
|
12
13
|
const scopes = new Map();
|
|
@@ -353,6 +354,9 @@ export class Model {
|
|
|
353
354
|
this.connection = connection;
|
|
354
355
|
ConnectionManager.setDefault(connection);
|
|
355
356
|
}
|
|
357
|
+
static useIdentityMap(callback) {
|
|
358
|
+
return IdentityMap.run(callback);
|
|
359
|
+
}
|
|
356
360
|
static on(connection) {
|
|
357
361
|
const resolved = typeof connection === "string" ? ConnectionManager.require(connection) : connection;
|
|
358
362
|
const builder = new Builder(resolved, resolved.qualifyTable(this.getTable()));
|
|
@@ -813,6 +817,13 @@ export class Model {
|
|
|
813
817
|
await ObserverRegistry.dispatch("created", this);
|
|
814
818
|
await ObserverRegistry.dispatch("saved", this);
|
|
815
819
|
}
|
|
820
|
+
const identityMap = IdentityMap.current();
|
|
821
|
+
if (identityMap) {
|
|
822
|
+
const pk = this.getAttribute(constructor.primaryKey);
|
|
823
|
+
if (pk !== null && pk !== undefined && pk !== "") {
|
|
824
|
+
IdentityMap.set(constructor.getTable(), pk, this);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
816
827
|
return this;
|
|
817
828
|
}
|
|
818
829
|
updateTimestamps() {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ModelNotFoundError } from "../model/ModelNotFoundError.js";
|
|
2
|
+
import { IdentityMap } from "../model/IdentityMap.js";
|
|
2
3
|
export class Builder {
|
|
3
4
|
connection;
|
|
4
5
|
tableName;
|
|
@@ -589,13 +590,31 @@ export class Builder {
|
|
|
589
590
|
const results = await this.connection.query(sql, this.bindings);
|
|
590
591
|
const rows = Array.from(results);
|
|
591
592
|
if (this.model) {
|
|
593
|
+
const identityMap = IdentityMap.current();
|
|
594
|
+
const table = this.model.getTable();
|
|
595
|
+
const primaryKey = this.model.primaryKey || "id";
|
|
592
596
|
const models = rows.map((row) => {
|
|
597
|
+
if (identityMap) {
|
|
598
|
+
const pk = row[primaryKey];
|
|
599
|
+
if (pk !== null && pk !== undefined) {
|
|
600
|
+
const cached = IdentityMap.get(table, pk);
|
|
601
|
+
if (cached) {
|
|
602
|
+
return cached;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
593
606
|
const instance = new this.model(row);
|
|
594
607
|
instance.$exists = true;
|
|
595
608
|
instance.$original = { ...row };
|
|
596
609
|
if (typeof instance.setConnection === "function") {
|
|
597
610
|
instance.setConnection(this.connection);
|
|
598
611
|
}
|
|
612
|
+
if (identityMap) {
|
|
613
|
+
const pk = row[primaryKey];
|
|
614
|
+
if (pk !== null && pk !== undefined) {
|
|
615
|
+
IdentityMap.set(table, pk, instance);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
599
618
|
return instance;
|
|
600
619
|
});
|
|
601
620
|
if (this.eagerLoads.length > 0) {
|
package/package.json
CHANGED