@bunnykit/orm 0.1.3 → 0.1.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.
package/README.md CHANGED
@@ -18,11 +18,12 @@ An **Eloquent-inspired ORM** built specifically for [Bun](https://bun.sh)'s nati
18
18
  - 📦 **Multi-database** — SQLite, MySQL, and PostgreSQL support
19
19
  - 🔷 **Fully Typed** — Written in TypeScript with generics everywhere
20
20
  - 🏗️ **Schema Builder** — Programmatic table creation, indexes, foreign keys
21
- - 🔍 **Query Builder** — Chainable `where`, `join`, `orderBy`, `groupBy`, etc.
22
- - 🧬 **Eloquent-style Models** — Property attributes, defaults, casts, dirty tracking, soft deletes, scopes
21
+ - 🔍 **Query Builder** — Chainable `where`, `join`, `orderBy`, `groupBy`, date filters, conditional building, etc.
22
+ - 🧬 **Eloquent-style Models** — Property attributes, defaults, casts, dirty tracking, soft deletes, scopes, find-or-fail, first-or-create
23
23
  - 🔗 **Relations** — Standard, many-to-many, polymorphic, through, one-of-many, and relation queries
24
24
  - 👁️ **Observers** — Lifecycle hooks (`creating`, `created`, `updating`, `updated`, etc.)
25
25
  - 🚀 **Migrations & CLI** — Create, run, and rollback migrations from the command line
26
+ - ⚡ **Streaming** — `chunk`, `cursor`, `each`, and `lazy` for memory-efficient large dataset processing
26
27
 
27
28
  ---
28
29
 
@@ -261,6 +262,14 @@ User.where({ role: "admin", active: true });
261
262
  User.whereIn("id", [1, 2, 3]);
262
263
  User.whereNull("deleted_at");
263
264
  User.whereNotNull("email");
265
+ User.whereNot("status", "banned");
266
+
267
+ // Date filtering (cross-database)
268
+ Event.whereDate("happened_at", "2024-01-01");
269
+ Event.whereYear("created_at", ">=", 2023);
270
+ Event.whereMonth("birthday", 12);
271
+ Event.whereDay("anniversary", 14);
272
+ Event.whereTime("opened_at", "09:00:00");
264
273
 
265
274
  // Chaining
266
275
  const results = await User
@@ -271,6 +280,16 @@ const results = await User
271
280
  .offset(0)
272
281
  .get();
273
282
 
283
+ // Conditional building
284
+ User.when(filters.name, (q) => q.where("name", filters.name))
285
+ .when(filters.age, (q) => q.where("age", ">=", filters.age))
286
+ .unless(showAll, (q) => q.where("active", true))
287
+ .tap((q) => console.log(q.toSql()));
288
+
289
+ // Ordering convenience
290
+ Post.latest().first(); // orderBy created_at desc
291
+ Post.oldest("published_at"); // orderBy published_at asc
292
+
274
293
  // Aggregates
275
294
  const count = await User.where("active", true).count();
276
295
  const exists = await User.where("email", "test@example.com").exists();
@@ -288,6 +307,16 @@ const emails = await User.pluck("email");
288
307
  // First / Find
289
308
  const user = await User.where("email", "alice@example.com").first();
290
309
  const byId = await User.find(1);
310
+
311
+ // Find-or-Fail (throws if not found)
312
+ const user = await User.findOrFail(1);
313
+ const first = await User.firstOrFail();
314
+
315
+ // Streaming large datasets
316
+ await User.chunk(100, (users) => { ... });
317
+ await User.each(100, (user) => { ... });
318
+ for await (const user of User.cursor()) { ... }
319
+ for await (const user of User.lazy(500)) { ... }
291
320
  ```
292
321
 
293
322
  ---
@@ -345,7 +374,18 @@ user.getDirty(); // { name: "Charlie" }
345
374
  await user.save();
346
375
  await user.delete();
347
376
  await user.refresh();
377
+ await user.touch(); // update only timestamps
378
+ await user.load("posts"); // lazy eager loading
348
379
  user.toJSON(); // plain object
380
+
381
+ // Increment / Decrement
382
+ await user.increment("login_count");
383
+ await user.increment("login_count", 5, { last_login_at: new Date() });
384
+ await user.decrement("stock", 10);
385
+
386
+ // First-or-Create / Update-or-Create
387
+ const user = await User.firstOrCreate({ email: "alice@example.com" }, { name: "Alice" });
388
+ const user = await User.updateOrCreate({ email: "alice@example.com" }, { name: "Alice Smith" });
349
389
  ```
350
390
 
351
391
  ### Default Attributes
@@ -932,7 +972,7 @@ Bunny includes a full test suite built with `bun:test`.
932
972
  bun test
933
973
  ```
934
974
 
935
- 92 tests covering connection management, schema grammars, query builder, model CRUD, casts, scopes, soft deletes, relations, observers, migrations, and type generation.
975
+ 139 tests covering connection management, schema grammars, query builder, model CRUD, casts, scopes, soft deletes, relations, observers, migrations, type generation, lazy eager loading, find-or-fail, first-or-create, increment/decrement, touch, chunk/cursor/lazy streaming, date where clauses, conditional query building, whereNot, and latest/oldest.
936
976
 
937
977
  ---
938
978
 
@@ -9,6 +9,7 @@ export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
9
9
  export { Builder } from "./query/Builder.js";
10
10
  export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
11
11
  export type { ModelConstructor, GlobalScope, CastDefinition, CastsAttributes } from "./model/Model.js";
12
+ export { ModelNotFoundError } from "./model/ModelNotFoundError.js";
12
13
  export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
13
14
  export { MorphMap } from "./model/MorphMap.js";
14
15
  export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
package/dist/src/index.js CHANGED
@@ -7,6 +7,7 @@ export { MySqlGrammar } from "./schema/grammars/MySqlGrammar.js";
7
7
  export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
8
8
  export { Builder } from "./query/Builder.js";
9
9
  export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
10
+ export { ModelNotFoundError } from "./model/ModelNotFoundError.js";
10
11
  export { ObserverRegistry } from "./model/Observer.js";
11
12
  export { MorphMap } from "./model/MorphMap.js";
12
13
  export { MorphTo, MorphOne, MorphMany, MorphToMany } from "./model/MorphRelations.js";
@@ -109,12 +109,39 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
109
109
  static shouldAutoGeneratePrimaryKey(): Promise<boolean>;
110
110
  static create<M extends typeof Model>(this: M, attributes: Partial<InstanceType<M> extends Model<infer U> ? U : Record<string, any>>): Promise<InstanceType<M>>;
111
111
  static find<M extends typeof Model>(this: M, id: any): Promise<InstanceType<M> | null>;
112
+ static findOrFail<M extends typeof Model>(this: M, id: any): Promise<InstanceType<M>>;
112
113
  static first<M extends typeof Model>(this: M): Promise<InstanceType<M> | null>;
114
+ static firstOrFail<M extends typeof Model>(this: M): Promise<InstanceType<M>>;
115
+ static firstOrCreate<M extends typeof Model>(this: M, attributes?: Record<string, any>, values?: Record<string, any>): Promise<InstanceType<M>>;
116
+ static updateOrCreate<M extends typeof Model>(this: M, attributes: Record<string, any>, values?: Record<string, any>): Promise<InstanceType<M>>;
113
117
  static where<M extends typeof Model>(this: M, column: string | Record<string, any>, operator?: string | any, value?: any): Builder<InstanceType<M>>;
118
+ static orderBy<M extends typeof Model>(this: M, column: string, direction?: "asc" | "desc"): Builder<InstanceType<M>>;
114
119
  static whereIn<M extends typeof Model>(this: M, column: string, values: any[]): Builder<InstanceType<M>>;
115
120
  static whereNull<M extends typeof Model>(this: M, column: string): Builder<InstanceType<M>>;
116
121
  static whereNotNull<M extends typeof Model>(this: M, column: string): Builder<InstanceType<M>>;
117
122
  static orWhere<M extends typeof Model>(this: M, column: string | Record<string, any>, operator?: string | any, value?: any): Builder<InstanceType<M>>;
123
+ static whereNot<M extends typeof Model>(this: M, column: string | Record<string, any>, value?: any): Builder<InstanceType<M>>;
124
+ static orWhereNot<M extends typeof Model>(this: M, column: string | Record<string, any>, value?: any): Builder<InstanceType<M>>;
125
+ static whereDate<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
126
+ static orWhereDate<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
127
+ static whereDay<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
128
+ static orWhereDay<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
129
+ static whereMonth<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
130
+ static orWhereMonth<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
131
+ static whereYear<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
132
+ static orWhereYear<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
133
+ static whereTime<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
134
+ static orWhereTime<M extends typeof Model>(this: M, column: string, operator?: string | any, value?: any): Builder<InstanceType<M>>;
135
+ static latest<M extends typeof Model>(this: M, column?: string): Builder<InstanceType<M>>;
136
+ static oldest<M extends typeof Model>(this: M, column?: string): Builder<InstanceType<M>>;
137
+ static when<M extends typeof Model>(this: M, condition: any, callback: (query: Builder<any>) => void | Builder<any>, defaultCallback?: (query: Builder<any>) => void | Builder<any>): Builder<InstanceType<M>>;
138
+ static unless<M extends typeof Model>(this: M, condition: any, callback: (query: Builder<any>) => void | Builder<any>, defaultCallback?: (query: Builder<any>) => void | Builder<any>): Builder<InstanceType<M>>;
139
+ static tap<M extends typeof Model>(this: M, callback: (query: Builder<any>) => void | Builder<any>): Builder<InstanceType<M>>;
140
+ static take<M extends typeof Model>(this: M, count: number): Builder<InstanceType<M>>;
141
+ static skip<M extends typeof Model>(this: M, count: number): Builder<InstanceType<M>>;
142
+ static inRandomOrder<M extends typeof Model>(this: M): Builder<InstanceType<M>>;
143
+ static lockForUpdate<M extends typeof Model>(this: M): Builder<InstanceType<M>>;
144
+ static sharedLock<M extends typeof Model>(this: M): Builder<InstanceType<M>>;
118
145
  static with<M extends typeof Model>(this: M, ...relations: string[]): Builder<InstanceType<M>>;
119
146
  static withTrashed<M extends typeof Model>(this: M): Builder<InstanceType<M>>;
120
147
  static onlyTrashed<M extends typeof Model>(this: M): Builder<InstanceType<M>>;
@@ -131,6 +158,10 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
131
158
  static withMax<M extends typeof Model>(this: M, relationName: string, column: string, alias?: string): Builder<InstanceType<M>>;
132
159
  static all<M extends typeof Model>(this: M): Promise<InstanceType<M>[]>;
133
160
  static paginate<M extends typeof Model>(this: M, perPage?: number, page?: number): Promise<import("../query/Builder.js").Paginator<InstanceType<M>>>;
161
+ static chunk<M extends typeof Model>(this: M, count: number, callback: (items: InstanceType<M>[]) => void | Promise<void>): Promise<void>;
162
+ static each<M extends typeof Model>(this: M, count: number, callback: (item: InstanceType<M>) => void | Promise<void>): Promise<void>;
163
+ static cursor<M extends typeof Model>(this: M): AsyncGenerator<InstanceType<M>>;
164
+ static lazy<M extends typeof Model>(this: M, count?: number): AsyncGenerator<InstanceType<M>>;
134
165
  static eagerLoadRelations(models: Model[], relations: string[]): Promise<void>;
135
166
  static eagerLoadRelation(models: Model[], relationName: string): Promise<void>;
136
167
  fill(attributes: Partial<T>): this;
@@ -147,6 +178,11 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
147
178
  getDirty(): Partial<T>;
148
179
  isDirty(): boolean;
149
180
  save(): Promise<this>;
181
+ updateTimestamps(): void;
182
+ touch(): Promise<boolean>;
183
+ increment(column: string, amount?: number, extra?: Record<string, any>): Promise<this>;
184
+ decrement(column: string, amount?: number, extra?: Record<string, any>): Promise<this>;
185
+ load(...relations: string[]): Promise<this>;
150
186
  delete(): Promise<boolean>;
151
187
  restore(): Promise<boolean>;
152
188
  forceDelete(): Promise<boolean>;
@@ -4,6 +4,7 @@ import { ObserverRegistry } from "./Observer.js";
4
4
  import { MorphTo, MorphOne, MorphMany, MorphToMany } from "./MorphRelations.js";
5
5
  import { BelongsToMany } from "./BelongsToMany.js";
6
6
  import { Schema } from "../schema/Schema.js";
7
+ import { ModelNotFoundError } from "./ModelNotFoundError.js";
7
8
  const globalScopes = new WeakMap();
8
9
  function getGlobalScopes(model) {
9
10
  const scopes = new Map();
@@ -397,12 +398,44 @@ export class Model {
397
398
  static async find(id) {
398
399
  return this.query().find(id, this.primaryKey);
399
400
  }
401
+ static async findOrFail(id) {
402
+ const result = await this.find(id);
403
+ if (!result) {
404
+ throw new ModelNotFoundError(this.name, id);
405
+ }
406
+ return result;
407
+ }
400
408
  static async first() {
401
409
  return this.query().first();
402
410
  }
411
+ static async firstOrFail() {
412
+ const result = await this.first();
413
+ if (!result) {
414
+ throw new ModelNotFoundError(this.name);
415
+ }
416
+ return result;
417
+ }
418
+ static async firstOrCreate(attributes = {}, values = {}) {
419
+ const found = await this.where(attributes).first();
420
+ if (found)
421
+ return found;
422
+ return this.create({ ...attributes, ...values });
423
+ }
424
+ static async updateOrCreate(attributes, values = {}) {
425
+ const found = await this.where(attributes).first();
426
+ if (found) {
427
+ found.fill(values);
428
+ await found.save();
429
+ return found;
430
+ }
431
+ return this.create({ ...attributes, ...values });
432
+ }
403
433
  static where(column, operator, value) {
404
434
  return this.query().where(column, operator, value);
405
435
  }
436
+ static orderBy(column, direction) {
437
+ return this.query().orderBy(column, direction);
438
+ }
406
439
  static whereIn(column, values) {
407
440
  return this.query().whereIn(column, values);
408
441
  }
@@ -415,6 +448,72 @@ export class Model {
415
448
  static orWhere(column, operator, value) {
416
449
  return this.query().orWhere(column, operator, value);
417
450
  }
451
+ static whereNot(column, value) {
452
+ return this.query().whereNot(column, value);
453
+ }
454
+ static orWhereNot(column, value) {
455
+ return this.query().orWhereNot(column, value);
456
+ }
457
+ static whereDate(column, operator, value) {
458
+ return this.query().whereDate(column, operator, value);
459
+ }
460
+ static orWhereDate(column, operator, value) {
461
+ return this.query().orWhereDate(column, operator, value);
462
+ }
463
+ static whereDay(column, operator, value) {
464
+ return this.query().whereDay(column, operator, value);
465
+ }
466
+ static orWhereDay(column, operator, value) {
467
+ return this.query().orWhereDay(column, operator, value);
468
+ }
469
+ static whereMonth(column, operator, value) {
470
+ return this.query().whereMonth(column, operator, value);
471
+ }
472
+ static orWhereMonth(column, operator, value) {
473
+ return this.query().orWhereMonth(column, operator, value);
474
+ }
475
+ static whereYear(column, operator, value) {
476
+ return this.query().whereYear(column, operator, value);
477
+ }
478
+ static orWhereYear(column, operator, value) {
479
+ return this.query().orWhereYear(column, operator, value);
480
+ }
481
+ static whereTime(column, operator, value) {
482
+ return this.query().whereTime(column, operator, value);
483
+ }
484
+ static orWhereTime(column, operator, value) {
485
+ return this.query().orWhereTime(column, operator, value);
486
+ }
487
+ static latest(column) {
488
+ return this.query().latest(column);
489
+ }
490
+ static oldest(column) {
491
+ return this.query().oldest(column);
492
+ }
493
+ static when(condition, callback, defaultCallback) {
494
+ return this.query().when(condition, callback, defaultCallback);
495
+ }
496
+ static unless(condition, callback, defaultCallback) {
497
+ return this.query().unless(condition, callback, defaultCallback);
498
+ }
499
+ static tap(callback) {
500
+ return this.query().tap(callback);
501
+ }
502
+ static take(count) {
503
+ return this.query().take(count);
504
+ }
505
+ static skip(count) {
506
+ return this.query().skip(count);
507
+ }
508
+ static inRandomOrder() {
509
+ return this.query().inRandomOrder();
510
+ }
511
+ static lockForUpdate() {
512
+ return this.query().lockForUpdate();
513
+ }
514
+ static sharedLock() {
515
+ return this.query().sharedLock();
516
+ }
418
517
  static with(...relations) {
419
518
  return this.query().with(...relations);
420
519
  }
@@ -463,6 +562,18 @@ export class Model {
463
562
  static async paginate(perPage, page) {
464
563
  return this.query().paginate(perPage, page);
465
564
  }
565
+ static async chunk(count, callback) {
566
+ return this.query().chunk(count, callback);
567
+ }
568
+ static async each(count, callback) {
569
+ return this.query().each(count, callback);
570
+ }
571
+ static cursor() {
572
+ return this.query().cursor();
573
+ }
574
+ static lazy(count) {
575
+ return this.query().lazy(count);
576
+ }
466
577
  static async eagerLoadRelations(models, relations) {
467
578
  for (const relationName of relations) {
468
579
  if (relationName.includes(".")) {
@@ -673,6 +784,57 @@ export class Model {
673
784
  }
674
785
  return this;
675
786
  }
787
+ updateTimestamps() {
788
+ const constructor = this.constructor;
789
+ if (!constructor.timestamps)
790
+ return;
791
+ const now = this.freshTimestamp();
792
+ this.$attributes["updated_at"] = now;
793
+ if (!this.$exists) {
794
+ this.$attributes["created_at"] = now;
795
+ }
796
+ }
797
+ async touch() {
798
+ if (!this.$exists)
799
+ return false;
800
+ const constructor = this.constructor;
801
+ if (!constructor.timestamps)
802
+ return false;
803
+ const now = this.freshTimestamp();
804
+ const pk = this.getAttribute(constructor.primaryKey);
805
+ await new Builder(constructor.getConnection(), constructor.getTable())
806
+ .where(constructor.primaryKey, pk)
807
+ .update({ updated_at: now });
808
+ this.$attributes["updated_at"] = now;
809
+ this.$original = { ...this.$attributes };
810
+ return true;
811
+ }
812
+ async increment(column, amount = 1, extra = {}) {
813
+ const constructor = this.constructor;
814
+ const pk = this.getAttribute(constructor.primaryKey);
815
+ if (!pk)
816
+ return this;
817
+ const builder = new Builder(constructor.getConnection(), constructor.getTable())
818
+ .where(constructor.primaryKey, pk);
819
+ if (constructor.timestamps) {
820
+ extra = { ...extra, updated_at: this.freshTimestamp() };
821
+ }
822
+ await builder.increment(column, amount, extra);
823
+ this.$attributes[column] = (this.$attributes[column] || 0) + amount;
824
+ for (const [key, value] of Object.entries(extra)) {
825
+ this.$attributes[key] = value;
826
+ }
827
+ this.$original = { ...this.$attributes };
828
+ return this;
829
+ }
830
+ async decrement(column, amount = 1, extra = {}) {
831
+ return this.increment(column, -amount, extra);
832
+ }
833
+ async load(...relations) {
834
+ const constructor = this.constructor;
835
+ await constructor.eagerLoadRelations([this], relations);
836
+ return this;
837
+ }
676
838
  async delete() {
677
839
  const constructor = this.constructor;
678
840
  await ObserverRegistry.dispatch("deleting", this);
@@ -0,0 +1,5 @@
1
+ export declare class ModelNotFoundError extends Error {
2
+ modelName: string;
3
+ identifiers?: any;
4
+ constructor(modelName: string, identifiers?: any);
5
+ }
@@ -0,0 +1,13 @@
1
+ export class ModelNotFoundError extends Error {
2
+ modelName;
3
+ identifiers;
4
+ constructor(modelName, identifiers) {
5
+ const msg = identifiers !== undefined
6
+ ? `No query results for model [${modelName}] ${JSON.stringify(identifiers)}`
7
+ : `No query results for model [${modelName}]`;
8
+ super(msg);
9
+ this.name = "ModelNotFoundError";
10
+ this.modelName = modelName;
11
+ this.identifiers = identifiers;
12
+ }
13
+ }
@@ -25,23 +25,41 @@ export declare class Builder<T = Record<string, any>> {
25
25
  distinctFlag: boolean;
26
26
  model?: typeof Model;
27
27
  eagerLoads: string[];
28
+ randomOrderFlag: boolean;
29
+ lockMode?: string;
28
30
  constructor(connection: Connection, table: string);
29
31
  setModel(model: typeof Model): this;
30
32
  table(table: string): this;
31
33
  select(...columns: string[]): this;
32
34
  distinct(): this;
33
- where(column: string | Record<string, any>, operator?: string | any, value?: any, boolean?: "and" | "or", scope?: string): this;
34
- orWhere(column: string | Record<string, any>, operator?: string | any, value?: any): this;
35
+ where(column: string | Record<string, any> | ((query: Builder<T>) => void), operator?: string | any, value?: any, boolean?: "and" | "or", scope?: string): this;
36
+ private whereNested;
37
+ orWhere(column: string | Record<string, any> | ((query: Builder<T>) => void), operator?: string | any, value?: any): this;
38
+ whereNot(column: string | Record<string, any>, value?: any, boolean?: "and" | "or"): this;
39
+ orWhereNot(column: string | Record<string, any>, value?: any): this;
35
40
  whereIn(column: string, values: any[], boolean?: "and" | "or", scope?: string): this;
36
41
  whereNotIn(column: string, values: any[], boolean?: "and" | "or", scope?: string): this;
37
42
  whereNull(column: string, boolean?: "and" | "or", scope?: string): this;
38
43
  whereNotNull(column: string, boolean?: "and" | "or", scope?: string): this;
39
44
  whereBetween(column: string, values: [any, any], boolean?: "and" | "or", scope?: string): this;
40
45
  whereNotBetween(column: string, values: [any, any], boolean?: "and" | "or", scope?: string): this;
46
+ whereDate(column: string, operator?: string | any, value?: any, boolean?: "and" | "or"): this;
47
+ orWhereDate(column: string, operator?: string | any, value?: any): this;
48
+ whereDay(column: string, operator?: string | any, value?: any, boolean?: "and" | "or"): this;
49
+ orWhereDay(column: string, operator?: string | any, value?: any): this;
50
+ whereMonth(column: string, operator?: string | any, value?: any, boolean?: "and" | "or"): this;
51
+ orWhereMonth(column: string, operator?: string | any, value?: any): this;
52
+ whereYear(column: string, operator?: string | any, value?: any, boolean?: "and" | "or"): this;
53
+ orWhereYear(column: string, operator?: string | any, value?: any): this;
54
+ whereTime(column: string, operator?: string | any, value?: any, boolean?: "and" | "or"): this;
55
+ orWhereTime(column: string, operator?: string | any, value?: any): this;
41
56
  whereRaw(sql: string, boolean?: "and" | "or", scope?: string): this;
42
57
  whereColumn(first: string, operator: string, second: string, boolean?: "and" | "or"): this;
43
58
  whereExists(sql: string, boolean?: "and" | "or", not?: boolean): this;
44
59
  orderBy(column: string, direction?: "asc" | "desc"): this;
60
+ latest(column?: string): this;
61
+ oldest(column?: string): this;
62
+ inRandomOrder(): this;
45
63
  groupBy(...columns: string[]): this;
46
64
  having(column: string, operator: string, value: any): this;
47
65
  limit(count: number): this;
@@ -56,6 +74,9 @@ export declare class Builder<T = Record<string, any>> {
56
74
  withTrashed(): this;
57
75
  onlyTrashed(): this;
58
76
  scope(name: string, ...args: any[]): this;
77
+ when(condition: any, callback: (query: this) => void | this, defaultCallback?: (query: this) => void | this): this;
78
+ unless(condition: any, callback: (query: this) => void | this, defaultCallback?: (query: this) => void | this): this;
79
+ tap(callback: (query: this) => void | this): this;
59
80
  has(relationName: string, operator?: string | RelationConstraint, count?: number, callback?: RelationConstraint): this;
60
81
  orHas(relationName: string, operator?: string | RelationConstraint, count?: number, callback?: RelationConstraint): this;
61
82
  whereHas(relationName: string, callback?: RelationConstraint, operator?: string, count?: number): this;
@@ -74,7 +95,9 @@ export declare class Builder<T = Record<string, any>> {
74
95
  private wrap;
75
96
  private wrapValue;
76
97
  private escape;
98
+ private compileWhereClause;
77
99
  private compileWheres;
100
+ private compileNestedWheres;
78
101
  private compileOrders;
79
102
  private compileGroups;
80
103
  private compileHavings;
@@ -86,6 +109,10 @@ export declare class Builder<T = Record<string, any>> {
86
109
  get(): Promise<T[]>;
87
110
  first(): Promise<T | null>;
88
111
  find(id: any, column?: string): Promise<T | null>;
112
+ findOrFail(id: any, column?: string): Promise<T>;
113
+ firstOrFail(): Promise<T>;
114
+ firstOrCreate(attributes?: Partial<T>, values?: Partial<T>): Promise<T>;
115
+ updateOrCreate(attributes: Partial<T>, values?: Partial<T>): Promise<T>;
89
116
  pluck(column: string): Promise<any[]>;
90
117
  private aggregate;
91
118
  count(column?: string): Promise<number>;
@@ -94,12 +121,24 @@ export declare class Builder<T = Record<string, any>> {
94
121
  min(column: string): Promise<any>;
95
122
  max(column: string): Promise<any>;
96
123
  paginate(perPage?: number, page?: number): Promise<Paginator<T>>;
124
+ chunk(count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
125
+ each(count: number, callback: (item: T) => void | Promise<void>): Promise<void>;
126
+ cursor(): AsyncGenerator<T>;
127
+ lazy(count?: number): AsyncGenerator<T>;
97
128
  insert(data: Partial<T> | Partial<T>[]): Promise<any>;
98
129
  insertGetId(data: Partial<T>, idColumn?: string): Promise<any>;
99
130
  update(data: Partial<T>): Promise<any>;
100
131
  delete(): Promise<any>;
132
+ increment(column: string, amount?: number, extra?: Record<string, any>): Promise<any>;
133
+ decrement(column: string, amount?: number, extra?: Record<string, any>): Promise<any>;
101
134
  restore(): Promise<any>;
102
135
  exists(): Promise<boolean>;
136
+ doesntExist(): Promise<boolean>;
137
+ take(count: number): this;
138
+ skip(count: number): this;
139
+ lockForUpdate(): this;
140
+ sharedLock(): this;
141
+ private addDateWhere;
103
142
  private getModelRelation;
104
143
  private withAggregate;
105
144
  }
@@ -1,3 +1,4 @@
1
+ import { ModelNotFoundError } from "../model/ModelNotFoundError.js";
1
2
  export class Builder {
2
3
  connection;
3
4
  tableName;
@@ -12,6 +13,8 @@ export class Builder {
12
13
  distinctFlag = false;
13
14
  model;
14
15
  eagerLoads = [];
16
+ randomOrderFlag = false;
17
+ lockMode;
15
18
  constructor(connection, table) {
16
19
  this.connection = connection;
17
20
  this.tableName = table;
@@ -33,6 +36,9 @@ export class Builder {
33
36
  return this;
34
37
  }
35
38
  where(column, operator, value, boolean = "and", scope) {
39
+ if (typeof column === "function") {
40
+ return this.whereNested(column, boolean);
41
+ }
36
42
  if (typeof column === "object" && column !== null) {
37
43
  for (const [key, val] of Object.entries(column)) {
38
44
  this.where(key, "=", val, boolean, scope);
@@ -46,9 +52,30 @@ export class Builder {
46
52
  this.wheres.push({ type: "basic", column, operator, value, boolean, scope });
47
53
  return this;
48
54
  }
55
+ whereNested(callback, boolean = "and") {
56
+ const nested = new Builder(this.connection, this.tableName);
57
+ callback(nested);
58
+ if (nested.wheres.length > 0) {
59
+ const sql = this.compileNestedWheres(nested);
60
+ this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
61
+ }
62
+ return this;
63
+ }
49
64
  orWhere(column, operator, value) {
50
65
  return this.where(column, operator, value, "or");
51
66
  }
67
+ whereNot(column, value, boolean = "and") {
68
+ if (typeof column === "object" && column !== null) {
69
+ for (const [key, val] of Object.entries(column)) {
70
+ this.whereNot(key, val, boolean);
71
+ }
72
+ return this;
73
+ }
74
+ return this.where(column, "!=", value, boolean);
75
+ }
76
+ orWhereNot(column, value) {
77
+ return this.whereNot(column, value, "or");
78
+ }
52
79
  whereIn(column, values, boolean = "and", scope) {
53
80
  this.wheres.push({ type: "in", column, value: values, boolean, scope });
54
81
  return this;
@@ -73,6 +100,36 @@ export class Builder {
73
100
  this.wheres.push({ type: "between", column, value: values, boolean, operator: "NOT BETWEEN", scope });
74
101
  return this;
75
102
  }
103
+ whereDate(column, operator, value, boolean = "and") {
104
+ return this.addDateWhere("date", column, operator, value, boolean);
105
+ }
106
+ orWhereDate(column, operator, value) {
107
+ return this.whereDate(column, operator, value, "or");
108
+ }
109
+ whereDay(column, operator, value, boolean = "and") {
110
+ return this.addDateWhere("day", column, operator, value, boolean);
111
+ }
112
+ orWhereDay(column, operator, value) {
113
+ return this.whereDay(column, operator, value, "or");
114
+ }
115
+ whereMonth(column, operator, value, boolean = "and") {
116
+ return this.addDateWhere("month", column, operator, value, boolean);
117
+ }
118
+ orWhereMonth(column, operator, value) {
119
+ return this.whereMonth(column, operator, value, "or");
120
+ }
121
+ whereYear(column, operator, value, boolean = "and") {
122
+ return this.addDateWhere("year", column, operator, value, boolean);
123
+ }
124
+ orWhereYear(column, operator, value) {
125
+ return this.whereYear(column, operator, value, "or");
126
+ }
127
+ whereTime(column, operator, value, boolean = "and") {
128
+ return this.addDateWhere("time", column, operator, value, boolean);
129
+ }
130
+ orWhereTime(column, operator, value) {
131
+ return this.whereTime(column, operator, value, "or");
132
+ }
76
133
  whereRaw(sql, boolean = "and", scope) {
77
134
  this.wheres.push({ type: "raw", column: sql, boolean, scope });
78
135
  return this;
@@ -89,6 +146,16 @@ export class Builder {
89
146
  this.orders.push({ column, direction });
90
147
  return this;
91
148
  }
149
+ latest(column = "created_at") {
150
+ return this.orderBy(column, "desc");
151
+ }
152
+ oldest(column = "created_at") {
153
+ return this.orderBy(column, "asc");
154
+ }
155
+ inRandomOrder() {
156
+ this.randomOrderFlag = true;
157
+ return this;
158
+ }
92
159
  groupBy(...columns) {
93
160
  this.groups.push(...columns);
94
161
  return this;
@@ -154,6 +221,24 @@ export class Builder {
154
221
  const result = scope.call(this.model, this, ...args);
155
222
  return (result || this);
156
223
  }
224
+ when(condition, callback, defaultCallback) {
225
+ if (condition) {
226
+ const result = callback(this);
227
+ return (result || this);
228
+ }
229
+ else if (defaultCallback) {
230
+ const result = defaultCallback(this);
231
+ return (result || this);
232
+ }
233
+ return this;
234
+ }
235
+ unless(condition, callback, defaultCallback) {
236
+ return this.when(!condition, callback, defaultCallback);
237
+ }
238
+ tap(callback) {
239
+ const result = callback(this);
240
+ return (result || this);
241
+ }
157
242
  has(relationName, operator = ">=", count = 1, callback) {
158
243
  if (typeof operator === "function") {
159
244
  callback = operator;
@@ -233,6 +318,8 @@ export class Builder {
233
318
  cloned.distinctFlag = this.distinctFlag;
234
319
  cloned.model = this.model;
235
320
  cloned.eagerLoads = [...this.eagerLoads];
321
+ cloned.randomOrderFlag = this.randomOrderFlag;
322
+ cloned.lockMode = this.lockMode;
236
323
  return cloned;
237
324
  }
238
325
  wrapColumn(value) {
@@ -269,40 +356,57 @@ export class Builder {
269
356
  return value;
270
357
  return `'${String(value).replace(/'/g, "''")}'`;
271
358
  }
359
+ compileWhereClause(where, prefix) {
360
+ if (where.type === "basic") {
361
+ return `${prefix} ${this.wrap(where.column)} ${where.operator} ${this.escape(where.value)}`;
362
+ }
363
+ else if (where.type === "in") {
364
+ const op = where.operator === "NOT IN" ? "NOT IN" : "IN";
365
+ return `${prefix} ${this.wrap(where.column)} ${op} (${where.value.map((v) => this.escape(v)).join(", ")})`;
366
+ }
367
+ else if (where.type === "null") {
368
+ const op = where.operator === "NOT NULL" ? "IS NOT NULL" : "IS NULL";
369
+ return `${prefix} ${this.wrap(where.column)} ${op}`;
370
+ }
371
+ else if (where.type === "between") {
372
+ const op = where.operator === "NOT BETWEEN" ? "NOT BETWEEN" : "BETWEEN";
373
+ return `${prefix} ${this.wrap(where.column)} ${op} ${this.escape(where.value[0])} AND ${this.escape(where.value[1])}`;
374
+ }
375
+ else if (where.type === "raw") {
376
+ return `${prefix} ${where.column}`;
377
+ }
378
+ else if (where.type === "column") {
379
+ return `${prefix} ${this.wrap(where.column)} ${where.operator} ${this.wrap(where.value)}`;
380
+ }
381
+ else if (where.type === "exists") {
382
+ return `${prefix} ${where.operator} (${where.column})`;
383
+ }
384
+ return "";
385
+ }
272
386
  compileWheres() {
273
387
  if (this.wheres.length === 0)
274
388
  return "";
275
389
  const clauses = this.wheres.map((where, index) => {
276
390
  const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
277
- if (where.type === "basic") {
278
- return `${prefix} ${this.wrap(where.column)} ${where.operator} ${this.escape(where.value)}`;
279
- }
280
- else if (where.type === "in") {
281
- const op = where.operator === "NOT IN" ? "NOT IN" : "IN";
282
- return `${prefix} ${this.wrap(where.column)} ${op} (${where.value.map((v) => this.escape(v)).join(", ")})`;
283
- }
284
- else if (where.type === "null") {
285
- const op = where.operator === "NOT NULL" ? "IS NOT NULL" : "IS NULL";
286
- return `${prefix} ${this.wrap(where.column)} ${op}`;
287
- }
288
- else if (where.type === "between") {
289
- const op = where.operator === "NOT BETWEEN" ? "NOT BETWEEN" : "BETWEEN";
290
- return `${prefix} ${this.wrap(where.column)} ${op} ${this.escape(where.value[0])} AND ${this.escape(where.value[1])}`;
291
- }
292
- else if (where.type === "raw") {
293
- return `${prefix} ${where.column}`;
294
- }
295
- else if (where.type === "column") {
296
- return `${prefix} ${this.wrap(where.column)} ${where.operator} ${this.wrap(where.value)}`;
297
- }
298
- else if (where.type === "exists") {
299
- return `${prefix} ${where.operator} (${where.column})`;
300
- }
301
- return "";
391
+ return this.compileWhereClause(where, prefix);
302
392
  });
303
393
  return clauses.join(" ");
304
394
  }
395
+ compileNestedWheres(builder) {
396
+ if (builder.wheres.length === 0)
397
+ return "";
398
+ const clauses = builder.wheres.map((where, index) => {
399
+ const prefix = index === 0 ? "" : where.boolean.toUpperCase();
400
+ return this.compileWhereClause(where, prefix);
401
+ });
402
+ return clauses.join(" ").trim();
403
+ }
305
404
  compileOrders() {
405
+ if (this.randomOrderFlag) {
406
+ const driver = this.connection.getDriverName();
407
+ const fn = driver === "mysql" ? "RAND()" : "RANDOM()";
408
+ return `ORDER BY ${fn}`;
409
+ }
306
410
  if (this.orders.length === 0)
307
411
  return "";
308
412
  return `ORDER BY ${this.orders.map((o) => `${this.wrap(o.column)} ${o.direction.toUpperCase()}`).join(", ")}`;
@@ -325,7 +429,10 @@ export class Builder {
325
429
  compileOffset() {
326
430
  if (this.offsetValue === undefined)
327
431
  return "";
328
- return `OFFSET ${this.offsetValue}`;
432
+ const limitSql = this.limitValue === undefined && this.connection.getDriverName() === "sqlite"
433
+ ? "LIMIT -1 "
434
+ : "";
435
+ return `${limitSql}OFFSET ${this.offsetValue}`;
329
436
  }
330
437
  compileColumns() {
331
438
  return this.columns.map((c) => (this.isRawColumn(c) ? c : this.wrap(c))).join(", ");
@@ -344,6 +451,8 @@ export class Builder {
344
451
  sql += " " + this.compileOrders();
345
452
  sql += " " + this.compileLimit();
346
453
  sql += " " + this.compileOffset();
454
+ if (this.lockMode)
455
+ sql += " " + this.lockMode;
347
456
  return sql.replace(/\s+/g, " ").trim();
348
457
  }
349
458
  async get() {
@@ -370,6 +479,44 @@ export class Builder {
370
479
  async find(id, column = "id") {
371
480
  return this.where(column, id).first();
372
481
  }
482
+ async findOrFail(id, column = "id") {
483
+ const result = await this.find(id, column);
484
+ if (!result) {
485
+ throw new ModelNotFoundError(this.model?.name || "Model", id);
486
+ }
487
+ return result;
488
+ }
489
+ async firstOrFail() {
490
+ const result = await this.first();
491
+ if (!result) {
492
+ throw new ModelNotFoundError(this.model?.name || "Model");
493
+ }
494
+ return result;
495
+ }
496
+ async firstOrCreate(attributes = {}, values = {}) {
497
+ const found = await this.clone().where(attributes).first();
498
+ if (found)
499
+ return found;
500
+ if (!this.model) {
501
+ throw new Error("firstOrCreate requires a model to be set on the builder");
502
+ }
503
+ return this.model.create({ ...attributes, ...values });
504
+ }
505
+ async updateOrCreate(attributes, values = {}) {
506
+ const found = await this.clone().where(attributes).first();
507
+ if (found) {
508
+ const model = found;
509
+ if (typeof model.fill === "function") {
510
+ model.fill(values);
511
+ await model.save();
512
+ }
513
+ return found;
514
+ }
515
+ if (!this.model) {
516
+ throw new Error("updateOrCreate requires a model to be set on the builder");
517
+ }
518
+ return this.model.create({ ...attributes, ...values });
519
+ }
373
520
  async pluck(column) {
374
521
  const results = await this.select(column).get();
375
522
  return results.map((row) => row[column]);
@@ -409,6 +556,49 @@ export class Builder {
409
556
  to: total === 0 ? 0 : Math.min(page * perPage, total),
410
557
  };
411
558
  }
559
+ async chunk(count, callback) {
560
+ let page = 1;
561
+ while (true) {
562
+ const items = await this.clone().forPage(page, count).get();
563
+ if (items.length === 0)
564
+ break;
565
+ await callback(items);
566
+ if (items.length < count)
567
+ break;
568
+ page++;
569
+ }
570
+ }
571
+ async each(count, callback) {
572
+ await this.chunk(count, async (items) => {
573
+ for (const item of items) {
574
+ await callback(item);
575
+ }
576
+ });
577
+ }
578
+ async *cursor() {
579
+ let offset = 0;
580
+ while (true) {
581
+ const items = await this.clone().offset(offset).limit(1).get();
582
+ if (items.length === 0)
583
+ break;
584
+ yield items[0];
585
+ offset++;
586
+ }
587
+ }
588
+ async *lazy(count = 1000) {
589
+ let page = 1;
590
+ while (true) {
591
+ const items = await this.clone().forPage(page, count).get();
592
+ if (items.length === 0)
593
+ break;
594
+ for (const item of items) {
595
+ yield item;
596
+ }
597
+ if (items.length < count)
598
+ break;
599
+ page++;
600
+ }
601
+ }
412
602
  async insert(data) {
413
603
  const records = Array.isArray(data) ? data : [data];
414
604
  if (records.length === 0)
@@ -435,6 +625,17 @@ export class Builder {
435
625
  const sql = `DELETE FROM ${this.wrap(this.tableName)} ${this.compileWheres()}`;
436
626
  return await this.connection.run(sql.trim());
437
627
  }
628
+ async increment(column, amount = 1, extra = {}) {
629
+ const sets = [`${this.wrap(column)} = ${this.wrap(column)} + ${amount}`];
630
+ for (const [key, value] of Object.entries(extra)) {
631
+ sets.push(`${this.wrap(key)} = ${this.escape(value)}`);
632
+ }
633
+ const sql = `UPDATE ${this.wrap(this.tableName)} SET ${sets.join(", ")} ${this.compileWheres()}`;
634
+ return await this.connection.run(sql.trim());
635
+ }
636
+ async decrement(column, amount = 1, extra = {}) {
637
+ return this.increment(column, -amount, extra);
638
+ }
438
639
  async restore() {
439
640
  const model = this.model;
440
641
  if (!model?.softDeletes) {
@@ -446,6 +647,87 @@ export class Builder {
446
647
  const result = await this.select("1 as exists_check").limit(1).get();
447
648
  return result.length > 0;
448
649
  }
650
+ async doesntExist() {
651
+ return !(await this.exists());
652
+ }
653
+ take(count) {
654
+ return this.limit(count);
655
+ }
656
+ skip(count) {
657
+ return this.offset(count);
658
+ }
659
+ lockForUpdate() {
660
+ const driver = this.connection.getDriverName();
661
+ if (driver !== "sqlite") {
662
+ this.lockMode = driver === "mysql" ? "FOR UPDATE" : "FOR UPDATE";
663
+ }
664
+ return this;
665
+ }
666
+ sharedLock() {
667
+ const driver = this.connection.getDriverName();
668
+ if (driver === "mysql") {
669
+ this.lockMode = "LOCK IN SHARE MODE";
670
+ }
671
+ else if (driver === "postgres") {
672
+ this.lockMode = "FOR SHARE";
673
+ }
674
+ return this;
675
+ }
676
+ addDateWhere(type, column, operator, value, boolean = "and") {
677
+ if (value === undefined) {
678
+ value = operator;
679
+ operator = "=";
680
+ }
681
+ const driver = this.connection.getDriverName();
682
+ const wrapped = this.wrap(column);
683
+ let sql;
684
+ switch (type) {
685
+ case "date":
686
+ if (driver === "sqlite")
687
+ sql = `date(${wrapped}) ${operator} ${this.escape(value)}`;
688
+ else if (driver === "mysql")
689
+ sql = `DATE(${wrapped}) ${operator} ${this.escape(value)}`;
690
+ else
691
+ sql = `(${wrapped})::date ${operator} ${this.escape(value)}`;
692
+ break;
693
+ case "day":
694
+ if (driver === "sqlite")
695
+ sql = `CAST(strftime('%d', ${wrapped}) AS INTEGER) ${operator} ${this.escape(value)}`;
696
+ else if (driver === "mysql")
697
+ sql = `DAY(${wrapped}) ${operator} ${this.escape(value)}`;
698
+ else
699
+ sql = `EXTRACT(DAY FROM ${wrapped}) ${operator} ${this.escape(value)}`;
700
+ break;
701
+ case "month":
702
+ if (driver === "sqlite")
703
+ sql = `CAST(strftime('%m', ${wrapped}) AS INTEGER) ${operator} ${this.escape(value)}`;
704
+ else if (driver === "mysql")
705
+ sql = `MONTH(${wrapped}) ${operator} ${this.escape(value)}`;
706
+ else
707
+ sql = `EXTRACT(MONTH FROM ${wrapped}) ${operator} ${this.escape(value)}`;
708
+ break;
709
+ case "year":
710
+ if (driver === "sqlite")
711
+ sql = `CAST(strftime('%Y', ${wrapped}) AS INTEGER) ${operator} ${this.escape(value)}`;
712
+ else if (driver === "mysql")
713
+ sql = `YEAR(${wrapped}) ${operator} ${this.escape(value)}`;
714
+ else
715
+ sql = `EXTRACT(YEAR FROM ${wrapped}) ${operator} ${this.escape(value)}`;
716
+ break;
717
+ case "time":
718
+ if (driver === "sqlite")
719
+ sql = `time(${wrapped}) ${operator} ${this.escape(value)}`;
720
+ else if (driver === "mysql")
721
+ sql = `TIME(${wrapped}) ${operator} ${this.escape(value)}`;
722
+ else
723
+ sql = `(${wrapped})::time ${operator} ${this.escape(value)}`;
724
+ break;
725
+ default:
726
+ sql = `${wrapped} ${operator} ${this.escape(value)}`;
727
+ }
728
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
729
+ return this;
730
+ }
449
731
  getModelRelation(relationName) {
450
732
  if (!this.model) {
451
733
  throw new Error(`Cannot query relation "${relationName}" without a model`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",