@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.
@@ -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>>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",