@bunnykit/orm 0.1.26 → 0.1.27

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/README.md CHANGED
@@ -972,6 +972,48 @@ class User extends Model {
972
972
  const post = await user.latestPost().getResults();
973
973
  ```
974
974
 
975
+ ### Eager Loading
976
+
977
+ Use `with()` to eager load relations for query results:
978
+
979
+ ```ts
980
+ const users = await User.with("posts", "profile").get();
981
+ const posts = await Post.with("author").get();
982
+ ```
983
+
984
+ Nested relations use dot notation:
985
+
986
+ ```ts
987
+ const users = await User.with("posts.comments").get();
988
+ ```
989
+
990
+ You can constrain an eager-loaded relation by passing an object whose key is the relation name and whose value is a query callback:
991
+
992
+ ```ts
993
+ const users = await User.with({
994
+ posts: (query) => {
995
+ query.where("status", "published").orderBy("created_at", "desc");
996
+ },
997
+ }).get();
998
+ ```
999
+
1000
+ Nested eager loads can be constrained too:
1001
+
1002
+ ```ts
1003
+ const users = await User.with(
1004
+ { posts: (query) => query.where("status", "published") },
1005
+ { "posts.comments": (query) => query.where("approved", true) },
1006
+ ).get();
1007
+ ```
1008
+
1009
+ Constrained eager loading is also available when loading relations onto an existing model:
1010
+
1011
+ ```ts
1012
+ await user.load({
1013
+ posts: (query) => query.where("status", "published"),
1014
+ });
1015
+ ```
1016
+
975
1017
  ### Relation Queries and Aggregates
976
1018
 
977
1019
  Filter models by related records:
@@ -14,7 +14,7 @@ export { MySqlGrammar } from "./schema/grammars/MySqlGrammar.js";
14
14
  export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
15
15
  export { Builder } from "./query/Builder.js";
16
16
  export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
17
- export type { ModelAttributeInput, ModelAttributes, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName, GlobalScope, CastDefinition, CastsAttributes, } from "./model/Model.js";
17
+ export type { ModelAttributeInput, ModelAttributes, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName, EagerLoadConstraint, EagerLoadDefinition, EagerLoadInput, GlobalScope, CastDefinition, CastsAttributes, } from "./model/Model.js";
18
18
  export { ModelNotFoundError } from "./model/ModelNotFoundError.js";
19
19
  export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
20
20
  export { MorphMap } from "./model/MorphMap.js";
@@ -5,6 +5,12 @@ import { BelongsToMany } from "./BelongsToMany.js";
5
5
  export type ModelConstructor<T extends Model = Model> = (new (...args: any[]) => T) & Omit<typeof Model, "prototype">;
6
6
  export type GlobalScope = (builder: Builder<any>, model: ModelConstructor) => void;
7
7
  export type LiteralUnion<T extends string> = T | (string & {});
8
+ export type EagerLoadConstraint = (query: Builder<any>) => void | Builder<any>;
9
+ export interface EagerLoadDefinition {
10
+ name: string;
11
+ constraint?: EagerLoadConstraint;
12
+ }
13
+ export type EagerLoadInput = string | EagerLoadDefinition | Record<string, EagerLoadConstraint | undefined>;
8
14
  type BaseModelInstanceKey = "$attributes" | "$original" | "$exists" | "$relations" | "$casts" | "$castCache" | "$connection" | "fill" | "setConnection" | "getConnection" | "isFillable" | "getAttribute" | "setAttribute" | "castAttribute" | "serializeCastAttribute" | "mergeCasts" | "getDirty" | "isDirty" | "save" | "updateTimestamps" | "touch" | "increment" | "decrement" | "load" | "delete" | "restore" | "forceDelete" | "refresh" | "toJSON" | "toString" | "freshTimestamp" | "setRelation" | "getRelation" | "hasMany" | "belongsTo" | "hasOne" | "hasManyThrough" | "hasOneThrough" | "belongsToMany" | "morphTo" | "morphOne" | "morphMany" | "morphToMany" | "morphedByMany";
9
15
  export type ModelInstanceAttributeKeys<T> = Extract<Exclude<keyof T, BaseModelInstanceKey>, string>;
10
16
  export type ModelAttributes<T> = T extends {
@@ -168,7 +174,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
168
174
  static inRandomOrder<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
169
175
  static lockForUpdate<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
170
176
  static sharedLock<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
171
- static with<M extends ModelConstructor>(this: M, ...relations: ModelRelationName<InstanceType<M>>[]): Builder<InstanceType<M>>;
177
+ static with<M extends ModelConstructor>(this: M, ...relations: (ModelRelationName<InstanceType<M>> | EagerLoadInput | EagerLoadInput[])[]): Builder<InstanceType<M>>;
172
178
  static withTrashed<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
173
179
  static onlyTrashed<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
174
180
  static withoutGlobalScope<M extends ModelConstructor>(this: M, scope: string): Builder<InstanceType<M>>;
@@ -188,8 +194,9 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
188
194
  static each<M extends ModelConstructor>(this: M, count: number, callback: (item: InstanceType<M>) => void | Promise<void>): Promise<void>;
189
195
  static cursor<M extends ModelConstructor>(this: M): AsyncGenerator<InstanceType<M>>;
190
196
  static lazy<M extends ModelConstructor>(this: M, count?: number): AsyncGenerator<InstanceType<M>>;
191
- static eagerLoadRelations(models: Model[], relations: string[]): Promise<void>;
192
- static eagerLoadRelation(models: Model[], relationName: string): Promise<void>;
197
+ static normalizeEagerLoads(relations: (EagerLoadInput | EagerLoadInput[])[]): EagerLoadDefinition[];
198
+ static eagerLoadRelations(models: Model[], relations: (string | EagerLoadDefinition)[]): Promise<void>;
199
+ static eagerLoadRelation(models: Model[], relationName: string, constraint?: EagerLoadConstraint): Promise<void>;
193
200
  fill(attributes: Partial<T> | ModelAttributeInput<this>): this;
194
201
  setConnection(connection: Connection): this;
195
202
  getConnection(): Connection;
@@ -210,7 +217,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
210
217
  touch(): Promise<boolean>;
211
218
  increment<K extends ModelColumn<this>>(column: K, amount?: number, extra?: ModelAttributeInput<this>): Promise<this>;
212
219
  decrement<K extends ModelColumn<this>>(column: K, amount?: number, extra?: ModelAttributeInput<this>): Promise<this>;
213
- load(...relations: string[]): Promise<this>;
220
+ load(...relations: (string | EagerLoadInput | EagerLoadInput[])[]): Promise<this>;
214
221
  delete(): Promise<boolean>;
215
222
  restore(): Promise<boolean>;
216
223
  forceDelete(): Promise<boolean>;
@@ -629,34 +629,65 @@ export class Model {
629
629
  static lazy(count) {
630
630
  return this.query().lazy(count);
631
631
  }
632
- static async eagerLoadRelations(models, relations) {
633
- for (const relationName of relations) {
634
- if (relationName.includes(".")) {
635
- const [first, ...rest] = relationName.split(".");
636
- await this.eagerLoadRelation(models, first);
637
- const nestedModels = [];
638
- for (const model of models) {
639
- const related = model.getRelation(first);
640
- if (Array.isArray(related))
641
- nestedModels.push(...related);
642
- else if (related)
643
- nestedModels.push(related);
644
- }
645
- if (nestedModels.length > 0) {
646
- await this.eagerLoadRelations(nestedModels, [rest.join(".")]);
647
- }
632
+ static normalizeEagerLoads(relations) {
633
+ const normalized = [];
634
+ for (const relation of relations.flat()) {
635
+ if (typeof relation === "string") {
636
+ normalized.push({ name: relation });
637
+ }
638
+ else if ("name" in relation && typeof relation.name === "string") {
639
+ normalized.push(relation);
648
640
  }
649
641
  else {
650
- await this.eagerLoadRelation(models, relationName);
642
+ for (const [name, constraint] of Object.entries(relation)) {
643
+ normalized.push({ name, constraint });
644
+ }
645
+ }
646
+ }
647
+ return normalized;
648
+ }
649
+ static async eagerLoadRelations(models, relations) {
650
+ const normalized = this.normalizeEagerLoads(relations);
651
+ const groups = new Map();
652
+ for (const definition of normalized) {
653
+ const [first] = definition.name.split(".");
654
+ const group = groups.get(first) || [];
655
+ group.push(definition);
656
+ groups.set(first, group);
657
+ }
658
+ for (const [relationName, definitions] of groups) {
659
+ const direct = definitions.find((definition) => definition.name === relationName);
660
+ await this.eagerLoadRelation(models, relationName, direct?.constraint);
661
+ const nestedDefinitions = definitions
662
+ .filter((definition) => definition.name.includes("."))
663
+ .map((definition) => ({
664
+ name: definition.name.split(".").slice(1).join("."),
665
+ constraint: definition.constraint,
666
+ }));
667
+ if (nestedDefinitions.length === 0)
668
+ continue;
669
+ const nestedModels = [];
670
+ for (const model of models) {
671
+ const related = model.getRelation(relationName);
672
+ if (Array.isArray(related))
673
+ nestedModels.push(...related);
674
+ else if (related)
675
+ nestedModels.push(related);
676
+ }
677
+ if (nestedModels.length > 0) {
678
+ await this.eagerLoadRelations(nestedModels, nestedDefinitions);
651
679
  }
652
680
  }
653
681
  }
654
- static async eagerLoadRelation(models, relationName) {
682
+ static async eagerLoadRelation(models, relationName, constraint) {
655
683
  if (models.length === 0)
656
684
  return;
657
685
  const firstModel = models[0];
658
686
  const relation = firstModel[relationName]();
659
687
  relation.addEagerConstraints(models);
688
+ if (constraint) {
689
+ constraint(relation.getQuery());
690
+ }
660
691
  const results = await relation.getEager();
661
692
  relation.match(models, results, relationName);
662
693
  }
@@ -1,6 +1,6 @@
1
1
  import { Connection } from "../connection/Connection.js";
2
2
  import type { WhereClause, OrderClause, HavingClause, UnionClause } from "../types/index.js";
3
- import type { ModelAttributeInput, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName } from "../model/Model.js";
3
+ import type { EagerLoadDefinition, EagerLoadInput, ModelAttributeInput, ModelColumn, ModelColumnValue, ModelConstructor } from "../model/Model.js";
4
4
  type RelationConstraint = (query: Builder<any>) => void | Builder<any>;
5
5
  export interface Paginator<T> {
6
6
  data: T[];
@@ -24,7 +24,7 @@ export declare class Builder<T = Record<string, any>> {
24
24
  joins: string[];
25
25
  distinctFlag: boolean;
26
26
  model?: ModelConstructor;
27
- eagerLoads: string[];
27
+ eagerLoads: EagerLoadDefinition[];
28
28
  randomOrderFlag: boolean;
29
29
  lockMode?: string;
30
30
  unions: UnionClause[];
@@ -36,6 +36,7 @@ export declare class Builder<T = Record<string, any>> {
36
36
  constructor(connection: Connection, table: string);
37
37
  private get grammar();
38
38
  private invalidateSqlCache;
39
+ private normalizeEagerLoads;
39
40
  setModel(model: ModelConstructor): this;
40
41
  table(table: string): this;
41
42
  select(...columns: ModelColumn<T>[]): this;
@@ -106,7 +107,7 @@ export declare class Builder<T = Record<string, any>> {
106
107
  crossJoin(table: string): this;
107
108
  union(query: Builder<T> | string, all?: boolean): this;
108
109
  unionAll(query: Builder<T> | string): this;
109
- with(...relations: ModelRelationName<T>[]): this;
110
+ with(...relations: (EagerLoadInput | EagerLoadInput[])[]): this;
110
111
  withoutGlobalScope(scope: string): this;
111
112
  withoutGlobalScopes(): this;
112
113
  withTrashed(): this;
@@ -32,6 +32,23 @@ export class Builder {
32
32
  invalidateSqlCache() {
33
33
  this.sqlCache = undefined;
34
34
  }
35
+ normalizeEagerLoads(relations) {
36
+ const normalized = [];
37
+ for (const relation of relations.flat()) {
38
+ if (typeof relation === "string") {
39
+ normalized.push({ name: relation });
40
+ }
41
+ else if ("name" in relation && typeof relation.name === "string") {
42
+ normalized.push(relation);
43
+ }
44
+ else {
45
+ for (const [name, constraint] of Object.entries(relation)) {
46
+ normalized.push({ name, constraint });
47
+ }
48
+ }
49
+ }
50
+ return normalized;
51
+ }
35
52
  setModel(model) {
36
53
  this.model = model;
37
54
  return this;
@@ -332,7 +349,7 @@ export class Builder {
332
349
  return this.union(query, true);
333
350
  }
334
351
  with(...relations) {
335
- this.eagerLoads.push(...relations);
352
+ this.eagerLoads.push(...this.normalizeEagerLoads(relations));
336
353
  return this;
337
354
  }
338
355
  withoutGlobalScope(scope) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
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",