@bunnykit/orm 0.1.18 → 0.1.20
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/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 +4 -0
- package/dist/src/model/Model.js +47 -17
- package/dist/src/query/Builder.js +19 -0
- package/package.json +1 -1
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
|
+
}
|
|
@@ -105,6 +105,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
105
105
|
static attributes: Record<string, any>;
|
|
106
106
|
static softDeletes: boolean;
|
|
107
107
|
static deletedAtColumn: string;
|
|
108
|
+
static preventLazyLoading: boolean;
|
|
108
109
|
$attributes: T;
|
|
109
110
|
$original: Partial<T>;
|
|
110
111
|
$exists: boolean;
|
|
@@ -112,9 +113,12 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
112
113
|
$casts: Record<string, CastDefinition>;
|
|
113
114
|
$connection?: Connection;
|
|
114
115
|
constructor(attributes?: Partial<T>);
|
|
116
|
+
private defineAttributeProperty;
|
|
117
|
+
private syncAttributeProperties;
|
|
115
118
|
static getTable(): string;
|
|
116
119
|
static getConnection(): Connection;
|
|
117
120
|
static setConnection(connection: Connection): void;
|
|
121
|
+
static useIdentityMap<T>(callback: () => T | Promise<T>): Promise<T>;
|
|
118
122
|
static on<M extends ModelConstructor>(this: M, connection: string | Connection): Builder<InstanceType<M>>;
|
|
119
123
|
static forTenant<M extends ModelConstructor>(this: M, tenantId: string): Builder<InstanceType<M>>;
|
|
120
124
|
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();
|
|
@@ -37,6 +38,15 @@ export class Relation {
|
|
|
37
38
|
this.builder = related.on(parent.getConnection());
|
|
38
39
|
this.foreignKey = foreignKey || this.defaultForeignKey();
|
|
39
40
|
this.localKey = localKey || related.primaryKey;
|
|
41
|
+
// Wrap getResults with lazy-loading guard
|
|
42
|
+
const originalGetResults = this.getResults.bind(this);
|
|
43
|
+
this.getResults = async () => {
|
|
44
|
+
if (this.parent.constructor.preventLazyLoading) {
|
|
45
|
+
throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
|
|
46
|
+
`Eager load the relation using with().`);
|
|
47
|
+
}
|
|
48
|
+
return await originalGetResults();
|
|
49
|
+
};
|
|
40
50
|
}
|
|
41
51
|
defaultForeignKey() {
|
|
42
52
|
return `${snakeCase(this.parent.constructor.name)}_id`;
|
|
@@ -290,6 +300,7 @@ export class Model {
|
|
|
290
300
|
static attributes = {};
|
|
291
301
|
static softDeletes = false;
|
|
292
302
|
static deletedAtColumn = "deleted_at";
|
|
303
|
+
static preventLazyLoading = false;
|
|
293
304
|
$attributes = {};
|
|
294
305
|
$original = {};
|
|
295
306
|
$exists = false;
|
|
@@ -304,6 +315,9 @@ export class Model {
|
|
|
304
315
|
if (attributes) {
|
|
305
316
|
this.fill(attributes);
|
|
306
317
|
}
|
|
318
|
+
this.syncAttributeProperties();
|
|
319
|
+
// Minimal Proxy fallback for dynamic property access on undefined keys.
|
|
320
|
+
// Pre-defined attribute getters/setters bypass the Proxy entirely.
|
|
307
321
|
return new Proxy(this, {
|
|
308
322
|
get(target, prop, receiver) {
|
|
309
323
|
if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
|
|
@@ -318,25 +332,23 @@ export class Model {
|
|
|
318
332
|
}
|
|
319
333
|
return Reflect.set(target, prop, value, receiver);
|
|
320
334
|
},
|
|
321
|
-
has(target, prop) {
|
|
322
|
-
return prop in target || (typeof prop === "string" && prop in target.$attributes);
|
|
323
|
-
},
|
|
324
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
325
|
-
if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
|
|
326
|
-
return {
|
|
327
|
-
enumerable: true,
|
|
328
|
-
configurable: true,
|
|
329
|
-
writable: true,
|
|
330
|
-
value: target.getAttribute(prop),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
334
|
-
},
|
|
335
|
-
ownKeys(target) {
|
|
336
|
-
return [...new Set([...Reflect.ownKeys(target), ...Object.keys(target.$attributes)])];
|
|
337
|
-
},
|
|
338
335
|
});
|
|
339
336
|
}
|
|
337
|
+
defineAttributeProperty(key) {
|
|
338
|
+
if (key in this)
|
|
339
|
+
return;
|
|
340
|
+
Object.defineProperty(this, key, {
|
|
341
|
+
get: () => this.getAttribute(key),
|
|
342
|
+
set: (value) => this.setAttribute(key, value),
|
|
343
|
+
enumerable: true,
|
|
344
|
+
configurable: true,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
syncAttributeProperties() {
|
|
348
|
+
for (const key of Object.keys(this.$attributes)) {
|
|
349
|
+
this.defineAttributeProperty(key);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
340
352
|
static getTable() {
|
|
341
353
|
return this.table || snakeCase(this.name) + "s";
|
|
342
354
|
}
|
|
@@ -353,6 +365,9 @@ export class Model {
|
|
|
353
365
|
this.connection = connection;
|
|
354
366
|
ConnectionManager.setDefault(connection);
|
|
355
367
|
}
|
|
368
|
+
static useIdentityMap(callback) {
|
|
369
|
+
return IdentityMap.run(callback);
|
|
370
|
+
}
|
|
356
371
|
static on(connection) {
|
|
357
372
|
const resolved = typeof connection === "string" ? ConnectionManager.require(connection) : connection;
|
|
358
373
|
const builder = new Builder(resolved, resolved.qualifyTable(this.getTable()));
|
|
@@ -658,6 +673,7 @@ export class Model {
|
|
|
658
673
|
}
|
|
659
674
|
setAttribute(key, value) {
|
|
660
675
|
this.$attributes[key] = this.serializeCastAttribute(key, value);
|
|
676
|
+
this.defineAttributeProperty(key);
|
|
661
677
|
}
|
|
662
678
|
castAttribute(key, value) {
|
|
663
679
|
const cast = this.getCastDefinition(key);
|
|
@@ -813,6 +829,14 @@ export class Model {
|
|
|
813
829
|
await ObserverRegistry.dispatch("created", this);
|
|
814
830
|
await ObserverRegistry.dispatch("saved", this);
|
|
815
831
|
}
|
|
832
|
+
this.syncAttributeProperties();
|
|
833
|
+
const identityMap = IdentityMap.current();
|
|
834
|
+
if (identityMap) {
|
|
835
|
+
const pk = this.getAttribute(constructor.primaryKey);
|
|
836
|
+
if (pk !== null && pk !== undefined && pk !== "") {
|
|
837
|
+
IdentityMap.set(constructor.getTable(), pk, this);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
816
840
|
return this;
|
|
817
841
|
}
|
|
818
842
|
updateTimestamps() {
|
|
@@ -824,6 +848,7 @@ export class Model {
|
|
|
824
848
|
if (!this.$exists) {
|
|
825
849
|
this.$attributes["created_at"] = now;
|
|
826
850
|
}
|
|
851
|
+
this.syncAttributeProperties();
|
|
827
852
|
}
|
|
828
853
|
async touch() {
|
|
829
854
|
if (!this.$exists)
|
|
@@ -839,6 +864,7 @@ export class Model {
|
|
|
839
864
|
.update({ updated_at: now });
|
|
840
865
|
this.$attributes["updated_at"] = now;
|
|
841
866
|
this.$original = { ...this.$attributes };
|
|
867
|
+
this.syncAttributeProperties();
|
|
842
868
|
return true;
|
|
843
869
|
}
|
|
844
870
|
async increment(column, amount = 1, extra = {}) {
|
|
@@ -858,6 +884,7 @@ export class Model {
|
|
|
858
884
|
this.$attributes[key] = value;
|
|
859
885
|
}
|
|
860
886
|
this.$original = { ...this.$attributes };
|
|
887
|
+
this.syncAttributeProperties();
|
|
861
888
|
return this;
|
|
862
889
|
}
|
|
863
890
|
async decrement(column, amount = 1, extra = {}) {
|
|
@@ -882,6 +909,7 @@ export class Model {
|
|
|
882
909
|
.update({ [constructor.deletedAtColumn]: deletedAt });
|
|
883
910
|
this.$attributes[constructor.deletedAtColumn] = deletedAt;
|
|
884
911
|
this.$original = { ...this.$attributes };
|
|
912
|
+
this.syncAttributeProperties();
|
|
885
913
|
}
|
|
886
914
|
else {
|
|
887
915
|
const connection = this.getConnection();
|
|
@@ -907,6 +935,7 @@ export class Model {
|
|
|
907
935
|
this.$attributes[constructor.deletedAtColumn] = null;
|
|
908
936
|
this.$original = { ...this.$attributes };
|
|
909
937
|
this.$exists = true;
|
|
938
|
+
this.syncAttributeProperties();
|
|
910
939
|
return true;
|
|
911
940
|
}
|
|
912
941
|
async forceDelete() {
|
|
@@ -930,6 +959,7 @@ export class Model {
|
|
|
930
959
|
if (result) {
|
|
931
960
|
this.$attributes = result.$attributes;
|
|
932
961
|
this.$original = { ...result.$attributes };
|
|
962
|
+
this.syncAttributeProperties();
|
|
933
963
|
}
|
|
934
964
|
return this;
|
|
935
965
|
}
|
|
@@ -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