@bunnykit/orm 0.1.19 → 0.1.21

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.
@@ -28,12 +28,12 @@ export class ConnectionManager {
28
28
  }
29
29
  const now = Date.now();
30
30
  const idleTimeout = poolConfig.idleTimeout || 30000;
31
+ // Reuse a healthy idle connection
31
32
  while (pool.length > 0) {
32
33
  const idx = pool.findIndex((c) => !c.inUse && (now - c.lastUsed) < idleTimeout);
33
34
  if (idx === -1)
34
35
  break;
35
36
  const pooled = pool[idx];
36
- pool.splice(idx, 1);
37
37
  try {
38
38
  await pooled.connection.query("SELECT 1");
39
39
  pooled.inUse = true;
@@ -41,6 +41,14 @@ export class ConnectionManager {
41
41
  }
42
42
  catch {
43
43
  await pooled.connection.close().catch(() => null);
44
+ pool.splice(idx, 1);
45
+ }
46
+ }
47
+ // Clean up expired idle connections
48
+ for (let i = pool.length - 1; i >= 0; i--) {
49
+ if (!pool[i].inUse && (now - pool[i].lastUsed) >= idleTimeout) {
50
+ await pool[i].connection.close().catch(() => null);
51
+ pool.splice(i, 1);
44
52
  }
45
53
  }
46
54
  if (pool.length < (poolConfig.maxConnections || 10)) {
@@ -203,6 +211,14 @@ export class ConnectionManager {
203
211
  this.connections.clear();
204
212
  this.pools.clear();
205
213
  this.tenantCache.clear();
214
+ // Reject all pending waiters
215
+ for (const [name, poolWaiters] of this.waiters) {
216
+ for (const waiter of poolWaiters) {
217
+ clearTimeout(waiter.timer);
218
+ waiter.reject(new Error(`Connection pool "${name}" is closing`));
219
+ }
220
+ }
221
+ this.waiters.clear();
206
222
  for (const connection of connections) {
207
223
  await connection.close();
208
224
  }
@@ -23,6 +23,15 @@ export class BelongsToMany {
23
23
  this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
24
24
  this.builder = related.on(parent.getConnection());
25
25
  this.addConstraints();
26
+ // Wrap getResults with lazy-loading guard
27
+ const originalGetResults = this.getResults.bind(this);
28
+ this.getResults = async () => {
29
+ if (this.parent.constructor.preventLazyLoading) {
30
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
31
+ `Eager load the relation using with().`);
32
+ }
33
+ return await originalGetResults();
34
+ };
26
35
  }
27
36
  addConstraints() {
28
37
  const relatedTable = this.related.getTable();
@@ -5,4 +5,5 @@ export declare class IdentityMap {
5
5
  static get(table: string, key: string | number): Model | undefined;
6
6
  static set(table: string, key: string | number, model: Model): void;
7
7
  static clear(): void;
8
+ static delete(table: string, key: string | number): void;
8
9
  }
@@ -25,4 +25,10 @@ export class IdentityMap {
25
25
  return;
26
26
  map.clear();
27
27
  }
28
+ static delete(table, key) {
29
+ const map = this.current();
30
+ if (!map)
31
+ return;
32
+ map.delete(`${table}:${String(key)}`);
33
+ }
28
34
  }
@@ -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,6 +113,8 @@ 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;
@@ -38,6 +38,15 @@ export class Relation {
38
38
  this.builder = related.on(parent.getConnection());
39
39
  this.foreignKey = foreignKey || this.defaultForeignKey();
40
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
+ };
41
50
  }
42
51
  defaultForeignKey() {
43
52
  return `${snakeCase(this.parent.constructor.name)}_id`;
@@ -291,6 +300,7 @@ export class Model {
291
300
  static attributes = {};
292
301
  static softDeletes = false;
293
302
  static deletedAtColumn = "deleted_at";
303
+ static preventLazyLoading = false;
294
304
  $attributes = {};
295
305
  $original = {};
296
306
  $exists = false;
@@ -305,6 +315,9 @@ export class Model {
305
315
  if (attributes) {
306
316
  this.fill(attributes);
307
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.
308
321
  return new Proxy(this, {
309
322
  get(target, prop, receiver) {
310
323
  if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
@@ -320,24 +333,55 @@ export class Model {
320
333
  return Reflect.set(target, prop, value, receiver);
321
334
  },
322
335
  has(target, prop) {
323
- return prop in target || (typeof prop === "string" && prop in target.$attributes);
336
+ if (typeof prop === "string" && prop in target.$attributes)
337
+ return true;
338
+ return Reflect.has(target, prop);
324
339
  },
325
340
  getOwnPropertyDescriptor(target, prop) {
326
- if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
341
+ if (typeof prop === "string" && prop in target.$attributes) {
327
342
  return {
328
343
  enumerable: true,
329
344
  configurable: true,
330
- writable: true,
331
345
  value: target.getAttribute(prop),
332
346
  };
333
347
  }
334
348
  return Reflect.getOwnPropertyDescriptor(target, prop);
335
349
  },
336
350
  ownKeys(target) {
337
- return [...new Set([...Reflect.ownKeys(target), ...Object.keys(target.$attributes)])];
351
+ const keys = new Set(Reflect.ownKeys(target));
352
+ for (const key of Object.keys(target.$attributes)) {
353
+ if (!key.startsWith("$"))
354
+ keys.add(key);
355
+ }
356
+ return Array.from(keys);
338
357
  },
339
358
  });
340
359
  }
360
+ defineAttributeProperty(key) {
361
+ if (key in this)
362
+ return;
363
+ Object.defineProperty(this, key, {
364
+ get: () => this.getAttribute(key),
365
+ set: (value) => this.setAttribute(key, value),
366
+ enumerable: true,
367
+ configurable: true,
368
+ });
369
+ }
370
+ syncAttributeProperties() {
371
+ const currentKeys = new Set(Object.keys(this.$attributes));
372
+ // Remove stale attribute properties
373
+ for (const key of Reflect.ownKeys(this)) {
374
+ if (key.startsWith("$") || typeof key !== "string")
375
+ continue;
376
+ const desc = Object.getOwnPropertyDescriptor(this, key);
377
+ if (desc && desc.get && desc.configurable && !currentKeys.has(key)) {
378
+ delete this[key];
379
+ }
380
+ }
381
+ for (const key of currentKeys) {
382
+ this.defineAttributeProperty(key);
383
+ }
384
+ }
341
385
  static getTable() {
342
386
  return this.table || snakeCase(this.name) + "s";
343
387
  }
@@ -662,6 +706,7 @@ export class Model {
662
706
  }
663
707
  setAttribute(key, value) {
664
708
  this.$attributes[key] = this.serializeCastAttribute(key, value);
709
+ this.defineAttributeProperty(key);
665
710
  }
666
711
  castAttribute(key, value) {
667
712
  const cast = this.getCastDefinition(key);
@@ -817,6 +862,7 @@ export class Model {
817
862
  await ObserverRegistry.dispatch("created", this);
818
863
  await ObserverRegistry.dispatch("saved", this);
819
864
  }
865
+ this.syncAttributeProperties();
820
866
  const identityMap = IdentityMap.current();
821
867
  if (identityMap) {
822
868
  const pk = this.getAttribute(constructor.primaryKey);
@@ -835,6 +881,7 @@ export class Model {
835
881
  if (!this.$exists) {
836
882
  this.$attributes["created_at"] = now;
837
883
  }
884
+ this.syncAttributeProperties();
838
885
  }
839
886
  async touch() {
840
887
  if (!this.$exists)
@@ -850,6 +897,7 @@ export class Model {
850
897
  .update({ updated_at: now });
851
898
  this.$attributes["updated_at"] = now;
852
899
  this.$original = { ...this.$attributes };
900
+ this.syncAttributeProperties();
853
901
  return true;
854
902
  }
855
903
  async increment(column, amount = 1, extra = {}) {
@@ -869,6 +917,7 @@ export class Model {
869
917
  this.$attributes[key] = value;
870
918
  }
871
919
  this.$original = { ...this.$attributes };
920
+ this.syncAttributeProperties();
872
921
  return this;
873
922
  }
874
923
  async decrement(column, amount = 1, extra = {}) {
@@ -893,6 +942,7 @@ export class Model {
893
942
  .update({ [constructor.deletedAtColumn]: deletedAt });
894
943
  this.$attributes[constructor.deletedAtColumn] = deletedAt;
895
944
  this.$original = { ...this.$attributes };
945
+ this.syncAttributeProperties();
896
946
  }
897
947
  else {
898
948
  const connection = this.getConnection();
@@ -901,6 +951,10 @@ export class Model {
901
951
  .delete();
902
952
  this.$exists = false;
903
953
  }
954
+ const identityMap = IdentityMap.current();
955
+ if (identityMap) {
956
+ IdentityMap.delete(constructor.getTable(), pk);
957
+ }
904
958
  await ObserverRegistry.dispatch("deleted", this);
905
959
  return true;
906
960
  }
@@ -918,6 +972,7 @@ export class Model {
918
972
  this.$attributes[constructor.deletedAtColumn] = null;
919
973
  this.$original = { ...this.$attributes };
920
974
  this.$exists = true;
975
+ this.syncAttributeProperties();
921
976
  return true;
922
977
  }
923
978
  async forceDelete() {
@@ -930,6 +985,10 @@ export class Model {
930
985
  .where(constructor.primaryKey, pk)
931
986
  .delete();
932
987
  this.$exists = false;
988
+ const identityMap = IdentityMap.current();
989
+ if (identityMap) {
990
+ IdentityMap.delete(constructor.getTable(), pk);
991
+ }
933
992
  return true;
934
993
  }
935
994
  async refresh() {
@@ -937,10 +996,20 @@ export class Model {
937
996
  const pk = this.getAttribute(constructor.primaryKey);
938
997
  if (!pk)
939
998
  return this;
999
+ // Bypass identity map to fetch fresh data
1000
+ const identityMap = IdentityMap.current();
1001
+ if (identityMap) {
1002
+ IdentityMap.delete(constructor.getTable(), pk);
1003
+ }
940
1004
  const result = await constructor.find(pk);
941
1005
  if (result) {
942
1006
  this.$attributes = result.$attributes;
943
1007
  this.$original = { ...result.$attributes };
1008
+ this.syncAttributeProperties();
1009
+ // Ensure this instance is the canonical one in the identity map
1010
+ if (identityMap) {
1011
+ IdentityMap.set(constructor.getTable(), pk, this);
1012
+ }
944
1013
  }
945
1014
  return this;
946
1015
  }
@@ -13,6 +13,15 @@ export class MorphTo {
13
13
  this.typeColumn = `${name}_type`;
14
14
  this.idColumn = `${name}_id`;
15
15
  this.typeMap = typeMap;
16
+ // Wrap getResults with lazy-loading guard
17
+ const originalGetResults = this.getResults.bind(this);
18
+ this.getResults = async () => {
19
+ if (this.parent.constructor.preventLazyLoading) {
20
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
21
+ `Eager load the relation using with().`);
22
+ }
23
+ return await originalGetResults();
24
+ };
16
25
  }
17
26
  async getResults() {
18
27
  const type = this.parent.getAttribute(this.typeColumn);
@@ -99,6 +108,15 @@ export class MorphOne {
99
108
  this.builder = related.on(parent.getConnection());
100
109
  this.builder.where(this.typeColumn, this.getMorphType());
101
110
  this.builder.where(this.idColumn, this.parent.getAttribute(this.localKey));
111
+ // Wrap getResults with lazy-loading guard
112
+ const originalGetResults = this.getResults.bind(this);
113
+ this.getResults = async () => {
114
+ if (this.parent.constructor.preventLazyLoading) {
115
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
116
+ `Eager load the relation using with().`);
117
+ }
118
+ return await originalGetResults();
119
+ };
102
120
  }
103
121
  getMorphType() {
104
122
  return this.parent.constructor.morphName || this.parent.constructor.name;
@@ -168,6 +186,15 @@ export class MorphMany {
168
186
  this.builder = related.on(parent.getConnection());
169
187
  this.builder.where(this.typeColumn, this.getMorphType());
170
188
  this.builder.where(this.idColumn, this.parent.getAttribute(this.localKey));
189
+ // Wrap getResults with lazy-loading guard
190
+ const originalGetResults = this.getResults.bind(this);
191
+ this.getResults = async () => {
192
+ if (this.parent.constructor.preventLazyLoading) {
193
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
194
+ `Eager load the relation using with().`);
195
+ }
196
+ return await originalGetResults();
197
+ };
171
198
  }
172
199
  getMorphType() {
173
200
  return this.parent.constructor.morphName || this.parent.constructor.name;
@@ -244,6 +271,15 @@ export class MorphToMany {
244
271
  this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
245
272
  this.builder = related.on(parent.getConnection());
246
273
  this.addConstraints();
274
+ // Wrap getResults with lazy-loading guard
275
+ const originalGetResults = this.getResults.bind(this);
276
+ this.getResults = async () => {
277
+ if (this.parent.constructor.preventLazyLoading) {
278
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
279
+ `Eager load the relation using with().`);
280
+ }
281
+ return await originalGetResults();
282
+ };
247
283
  }
248
284
  addConstraints() {
249
285
  const relatedTable = this.related.getTable();
@@ -182,10 +182,7 @@ export class Builder {
182
182
  return this.whereRaw(sql, "or", scope);
183
183
  }
184
184
  whereJsonContains(column, value, boolean = "and", not = false) {
185
- let sql = this.grammar.compileJsonContains(this.grammar.wrap(column), value);
186
- if (not)
187
- sql = `NOT (${sql})`;
188
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
185
+ this.wheres.push({ type: "json_contains", column, value, boolean, scope: undefined, not });
189
186
  return this;
190
187
  }
191
188
  whereJsonLength(column, operator = "=", value, boolean = "and", not = false) {
@@ -193,41 +190,31 @@ export class Builder {
193
190
  value = operator;
194
191
  operator = "=";
195
192
  }
196
- let sql = this.grammar.compileJsonLength(this.grammar.wrap(column), String(operator), value);
197
- if (not)
198
- sql = `NOT (${sql})`;
199
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
193
+ this.wheres.push({ type: "json_length", column, operator: String(operator), value, boolean, scope: undefined, not });
200
194
  return this;
201
195
  }
202
196
  whereLike(column, value, boolean = "and", not = false) {
203
- const sql = this.grammar.compileLike(this.grammar.wrap(column), value, not);
204
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
197
+ this.wheres.push({ type: "like", column, value, boolean, scope: undefined, not });
205
198
  return this;
206
199
  }
207
200
  whereNotLike(column, value) {
208
201
  return this.whereLike(column, value, "and", true);
209
202
  }
210
203
  whereRegexp(column, value, boolean = "and", not = false) {
211
- const sql = this.grammar.compileRegexp(this.grammar.wrap(column), value, not);
212
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
204
+ this.wheres.push({ type: "regexp", column, value, boolean, scope: undefined, not });
213
205
  return this;
214
206
  }
215
207
  whereFullText(columns, value, boolean = "and", not = false) {
216
208
  const cols = Array.isArray(columns) ? columns : [columns];
217
- let sql = this.grammar.compileFullText(cols.map((c) => this.grammar.wrap(c)), value);
218
- if (not)
219
- sql = `NOT (${sql})`;
220
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
209
+ this.wheres.push({ type: "fulltext", column: "", columns: cols, value, boolean, scope: undefined, not });
221
210
  return this;
222
211
  }
223
212
  whereAll(columns, operator, value, boolean = "and") {
224
- const sql = columns.map((c) => `${this.grammar.wrap(c)} ${operator} ${this.grammar.escape(value)}`).join(" AND ");
225
- this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
213
+ this.wheres.push({ type: "all", column: "", columns: columns, operator, value, boolean, scope: undefined });
226
214
  return this;
227
215
  }
228
216
  whereAny(columns, operator, value, boolean = "and") {
229
- const sql = columns.map((c) => `${this.grammar.wrap(c)} ${operator} ${this.grammar.escape(value)}`).join(" OR ");
230
- this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
217
+ this.wheres.push({ type: "any", column: "", columns: columns, operator, value, boolean, scope: undefined });
231
218
  return this;
232
219
  }
233
220
  orderBy(column, direction = "asc") {
@@ -459,6 +446,7 @@ export class Builder {
459
446
  cloned.fromRaw = this.fromRaw;
460
447
  cloned.updateJoins = [...this.updateJoins];
461
448
  cloned.bindings = [...this.bindings];
449
+ cloned.parameterize = this.parameterize;
462
450
  return cloned;
463
451
  }
464
452
  wrapColumn(value) {
@@ -496,6 +484,53 @@ export class Builder {
496
484
  else if (where.type === "raw") {
497
485
  return `${prefix} ${where.column}`;
498
486
  }
487
+ else if (where.type === "like") {
488
+ const sql = this.grammar.compileLike(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
489
+ return `${prefix} ${sql}`;
490
+ }
491
+ else if (where.type === "regexp") {
492
+ const sql = this.grammar.compileRegexp(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
493
+ return `${prefix} ${sql}`;
494
+ }
495
+ else if (where.type === "fulltext") {
496
+ const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
497
+ let sql = this.grammar.compileFullText(cols, where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
498
+ if (where.not)
499
+ sql = `NOT (${sql})`;
500
+ return `${prefix} ${sql}`;
501
+ }
502
+ else if (where.type === "json_contains") {
503
+ let sql = this.grammar.compileJsonContains(this.grammar.wrap(where.column), where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
504
+ if (where.not)
505
+ sql = `NOT (${sql})`;
506
+ return `${prefix} ${sql}`;
507
+ }
508
+ else if (where.type === "json_length") {
509
+ let sql = this.grammar.compileJsonLength(this.grammar.wrap(where.column), where.operator || "=", where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
510
+ if (where.not)
511
+ sql = `NOT (${sql})`;
512
+ return `${prefix} ${sql}`;
513
+ }
514
+ else if (where.type === "date") {
515
+ const sql = this.grammar.compileDateWhere(where.dateType || "date", this.grammar.wrap(where.column), where.operator || "=", where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
516
+ return `${prefix} ${sql}`;
517
+ }
518
+ else if (where.type === "all") {
519
+ const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
520
+ const inner = cols.map((c) => {
521
+ const val = this.parameterize ? this.addBinding(where.value) : this.grammar.escape(where.value);
522
+ return `${c} ${where.operator} ${val}`;
523
+ }).join(" AND ");
524
+ return `${prefix} (${inner})`;
525
+ }
526
+ else if (where.type === "any") {
527
+ const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
528
+ const inner = cols.map((c) => {
529
+ const val = this.parameterize ? this.addBinding(where.value) : this.grammar.escape(where.value);
530
+ return `${c} ${where.operator} ${val}`;
531
+ }).join(" OR ");
532
+ return `${prefix} (${inner})`;
533
+ }
499
534
  else if (where.type === "column") {
500
535
  return `${prefix} ${this.grammar.wrap(where.column)} ${where.operator} ${this.grammar.wrap(where.value)}`;
501
536
  }
@@ -740,16 +775,34 @@ export class Builder {
740
775
  async *cursor(chunkSize = 1000) {
741
776
  const model = this.model;
742
777
  const primaryKey = model ? model.primaryKey || "id" : "id";
778
+ // Cursor pagination is incompatible with random ordering
779
+ if (this.randomOrderFlag) {
780
+ throw new Error("cursor() does not support inRandomOrder(). Use lazy() instead.");
781
+ }
743
782
  const orderColumn = this.orders[0]?.column || primaryKey;
744
783
  const orderDirection = this.orders[0]?.direction || "asc";
784
+ // Use unqualified column name for property access on model instances
785
+ const accessColumn = orderColumn.includes(".") ? orderColumn.split(".")[1] : orderColumn;
745
786
  let lastValue = undefined;
746
787
  while (true) {
747
788
  const builder = this.clone();
748
- builder.orders = [{ column: orderColumn, direction: orderDirection }];
789
+ // Preserve multi-column ORDER BY, appending PK tie-breaker if not present
790
+ builder.orders = this.orders.length > 0 ? [...this.orders] : [{ column: orderColumn, direction: orderDirection }];
791
+ const hasPkOrder = builder.orders.some((o) => o.column === primaryKey);
792
+ if (!hasPkOrder) {
793
+ builder.orders.push({ column: primaryKey, direction: orderDirection });
794
+ }
749
795
  builder.offsetValue = undefined;
750
796
  builder.limitValue = chunkSize;
751
797
  if (lastValue !== undefined) {
752
798
  const op = orderDirection === "asc" ? ">" : "<";
799
+ // Parenthesize existing wheres when appending cursor condition to preserve OR precedence
800
+ if (builder.wheres.length > 0) {
801
+ const hasOr = builder.wheres.some((w) => w.boolean === "or");
802
+ if (hasOr) {
803
+ builder.wheres = [{ type: "raw", column: `(${builder.compileWheres().replace(/^WHERE /, "")})`, boolean: "and", scope: undefined }];
804
+ }
805
+ }
753
806
  builder.wheres.push({
754
807
  type: "basic",
755
808
  column: orderColumn,
@@ -769,7 +822,7 @@ export class Builder {
769
822
  break;
770
823
  const lastItem = items[items.length - 1];
771
824
  lastValue = lastItem && typeof lastItem === "object"
772
- ? lastItem[orderColumn]
825
+ ? lastItem[accessColumn]
773
826
  : undefined;
774
827
  }
775
828
  }
@@ -958,8 +1011,7 @@ export class Builder {
958
1011
  value = operator;
959
1012
  operator = "=";
960
1013
  }
961
- const sql = this.grammar.compileDateWhere(type, this.grammar.wrap(column), operator, value);
962
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
1014
+ this.wheres.push({ type: "date", column: column, operator, value, boolean, scope: undefined, dateType: type });
963
1015
  return this;
964
1016
  }
965
1017
  getModelRelation(relationName) {
@@ -6,15 +6,15 @@ export declare abstract class Grammar {
6
6
  abstract compileRandomOrder(): string;
7
7
  compileOffset(offset: number, _limit?: number): string;
8
8
  compileLock(lockMode?: string): string;
9
- abstract compileDateWhere(type: string, column: string, operator: string, value: any): string;
9
+ abstract compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
10
10
  abstract compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
11
11
  abstract compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
12
12
  compileUpdate(table: string, sets: string[], wheres: string, joins?: string[]): string;
13
13
  compileDelete(table: string, wheres: string, joins?: string[], limit?: number): string;
14
- abstract compileJsonContains(column: string, value: any): string;
15
- abstract compileJsonLength(column: string, operator: string, value: any): string;
16
- compileLike(column: string, value: string, not: boolean): string;
17
- abstract compileRegexp(column: string, value: string, not: boolean): string;
18
- abstract compileFullText(columns: string[], value: string): string;
14
+ abstract compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
15
+ abstract compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
16
+ compileLike(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
17
+ abstract compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
18
+ abstract compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
19
19
  abstract compileExplain(sql: string): string;
20
20
  }
@@ -40,8 +40,8 @@ export class Grammar {
40
40
  sql += ` LIMIT ${limit}`;
41
41
  return sql.trim();
42
42
  }
43
- compileLike(column, value, not) {
43
+ compileLike(column, value, not, binding) {
44
44
  const op = not ? "NOT LIKE" : "LIKE";
45
- return `${column} ${op} ${this.escape(value)}`;
45
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
46
46
  }
47
47
  }
@@ -3,12 +3,12 @@ export declare class MySqlGrammar extends Grammar {
3
3
  wrap(value: string): string;
4
4
  placeholder(_index: number): string;
5
5
  compileRandomOrder(): string;
6
- compileDateWhere(type: string, column: string, operator: string, value: any): string;
6
+ compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
7
7
  compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
8
8
  compileUpsert(table: string, columns: string[], values: string[], _uniqueBy: string[], updateColumns: string[]): string;
9
- compileJsonContains(column: string, value: any): string;
10
- compileJsonLength(column: string, operator: string, value: any): string;
11
- compileRegexp(column: string, value: string, not: boolean): string;
12
- compileFullText(columns: string[], value: string): string;
9
+ compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
10
+ compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
11
+ compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
12
+ compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
13
13
  compileExplain(sql: string): string;
14
14
  }
@@ -18,20 +18,21 @@ export class MySqlGrammar extends Grammar {
18
18
  compileRandomOrder() {
19
19
  return "ORDER BY RAND()";
20
20
  }
21
- compileDateWhere(type, column, operator, value) {
21
+ compileDateWhere(type, column, operator, value, binding) {
22
+ const val = binding ? binding(value) : this.escape(value);
22
23
  switch (type) {
23
24
  case "date":
24
- return `DATE(${column}) ${operator} ${this.escape(value)}`;
25
+ return `DATE(${column}) ${operator} ${val}`;
25
26
  case "day":
26
- return `DAY(${column}) ${operator} ${this.escape(value)}`;
27
+ return `DAY(${column}) ${operator} ${val}`;
27
28
  case "month":
28
- return `MONTH(${column}) ${operator} ${this.escape(value)}`;
29
+ return `MONTH(${column}) ${operator} ${val}`;
29
30
  case "year":
30
- return `YEAR(${column}) ${operator} ${this.escape(value)}`;
31
+ return `YEAR(${column}) ${operator} ${val}`;
31
32
  case "time":
32
- return `TIME(${column}) ${operator} ${this.escape(value)}`;
33
+ return `TIME(${column}) ${operator} ${val}`;
33
34
  default:
34
- return `${column} ${operator} ${this.escape(value)}`;
35
+ return `${column} ${operator} ${val}`;
35
36
  }
36
37
  }
37
38
  compileInsertOrIgnore(table, columns, values) {
@@ -43,18 +44,18 @@ export class MySqlGrammar extends Grammar {
43
44
  .join(", ");
44
45
  return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON DUPLICATE KEY UPDATE ${updateCols}`;
45
46
  }
46
- compileJsonContains(column, value) {
47
- return `JSON_CONTAINS(${column}, ${this.escape(JSON.stringify(value))})`;
47
+ compileJsonContains(column, value, binding) {
48
+ return `JSON_CONTAINS(${column}, ${binding ? binding(JSON.stringify(value)) : this.escape(JSON.stringify(value))})`;
48
49
  }
49
- compileJsonLength(column, operator, value) {
50
- return `JSON_LENGTH(${column}) ${operator} ${this.escape(value)}`;
50
+ compileJsonLength(column, operator, value, binding) {
51
+ return `JSON_LENGTH(${column}) ${operator} ${binding ? binding(value) : this.escape(value)}`;
51
52
  }
52
- compileRegexp(column, value, not) {
53
+ compileRegexp(column, value, not, binding) {
53
54
  const op = not ? "NOT REGEXP" : "REGEXP";
54
- return `${column} ${op} ${this.escape(value)}`;
55
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
55
56
  }
56
- compileFullText(columns, value) {
57
- return `MATCH (${columns.join(", ")}) AGAINST (${this.escape(value)})`;
57
+ compileFullText(columns, value, binding) {
58
+ return `MATCH (${columns.join(", ")}) AGAINST (${binding ? binding(value) : this.escape(value)})`;
58
59
  }
59
60
  compileExplain(sql) {
60
61
  return `EXPLAIN ${sql}`;
@@ -3,12 +3,12 @@ export declare class PostgresGrammar extends Grammar {
3
3
  wrap(value: string): string;
4
4
  placeholder(index: number): string;
5
5
  compileRandomOrder(): string;
6
- compileDateWhere(type: string, column: string, operator: string, value: any): string;
6
+ compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
7
7
  compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
8
8
  compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
9
- compileJsonContains(column: string, value: any): string;
10
- compileJsonLength(column: string, operator: string, value: any): string;
11
- compileRegexp(column: string, value: string, not: boolean): string;
12
- compileFullText(columns: string[], value: string): string;
9
+ compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
10
+ compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
11
+ compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
12
+ compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
13
13
  compileExplain(sql: string): string;
14
14
  }
@@ -18,20 +18,21 @@ export class PostgresGrammar extends Grammar {
18
18
  compileRandomOrder() {
19
19
  return "ORDER BY RANDOM()";
20
20
  }
21
- compileDateWhere(type, column, operator, value) {
21
+ compileDateWhere(type, column, operator, value, binding) {
22
+ const val = binding ? binding(value) : this.escape(value);
22
23
  switch (type) {
23
24
  case "date":
24
- return `(${column})::date ${operator} ${this.escape(value)}`;
25
+ return `(${column})::date ${operator} ${val}`;
25
26
  case "day":
26
- return `EXTRACT(DAY FROM ${column}) ${operator} ${this.escape(value)}`;
27
+ return `EXTRACT(DAY FROM ${column}) ${operator} ${val}`;
27
28
  case "month":
28
- return `EXTRACT(MONTH FROM ${column}) ${operator} ${this.escape(value)}`;
29
+ return `EXTRACT(MONTH FROM ${column}) ${operator} ${val}`;
29
30
  case "year":
30
- return `EXTRACT(YEAR FROM ${column}) ${operator} ${this.escape(value)}`;
31
+ return `EXTRACT(YEAR FROM ${column}) ${operator} ${val}`;
31
32
  case "time":
32
- return `(${column})::time ${operator} ${this.escape(value)}`;
33
+ return `(${column})::time ${operator} ${val}`;
33
34
  default:
34
- return `${column} ${operator} ${this.escape(value)}`;
35
+ return `${column} ${operator} ${val}`;
35
36
  }
36
37
  }
37
38
  compileInsertOrIgnore(table, columns, values) {
@@ -43,21 +44,21 @@ export class PostgresGrammar extends Grammar {
43
44
  .join(", ");
44
45
  return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON CONFLICT (${uniqueBy.map((c) => this.wrap(c)).join(", ")}) DO UPDATE SET ${updateCols}`;
45
46
  }
46
- compileJsonContains(column, value) {
47
- return `${column} @> ${this.escape(JSON.stringify([value]))}`;
47
+ compileJsonContains(column, value, binding) {
48
+ return `${column} @> ${binding ? binding(JSON.stringify([value])) : this.escape(JSON.stringify([value]))}`;
48
49
  }
49
- compileJsonLength(column, operator, value) {
50
- return `jsonb_array_length(${column}) ${operator} ${this.escape(value)}`;
50
+ compileJsonLength(column, operator, value, binding) {
51
+ return `jsonb_array_length(${column}) ${operator} ${binding ? binding(value) : this.escape(value)}`;
51
52
  }
52
- compileRegexp(column, value, not) {
53
+ compileRegexp(column, value, not, binding) {
53
54
  const op = not ? "!~" : "~";
54
- return `${column} ${op} ${this.escape(value)}`;
55
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
55
56
  }
56
- compileFullText(columns, value) {
57
+ compileFullText(columns, value, binding) {
57
58
  const cols = columns.length > 1
58
59
  ? `concat_ws(' ', ${columns.join(", ")})`
59
60
  : columns[0];
60
- return `to_tsvector('english', ${cols}) @@ plainto_tsquery('english', ${this.escape(value)})`;
61
+ return `to_tsvector('english', ${cols}) @@ plainto_tsquery('english', ${binding ? binding(value) : this.escape(value)})`;
61
62
  }
62
63
  compileExplain(sql) {
63
64
  return `EXPLAIN (FORMAT JSON) ${sql}`;
@@ -4,12 +4,12 @@ export declare class SQLiteGrammar extends Grammar {
4
4
  placeholder(_index: number): string;
5
5
  compileRandomOrder(): string;
6
6
  compileOffset(offset: number, limit?: number): string;
7
- compileDateWhere(type: string, column: string, operator: string, value: any): string;
7
+ compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
8
8
  compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
9
9
  compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
10
- compileJsonContains(column: string, value: any): string;
11
- compileJsonLength(column: string, operator: string, value: any): string;
12
- compileRegexp(column: string, value: string, not: boolean): string;
13
- compileFullText(columns: string[], value: string): string;
10
+ compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
11
+ compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
12
+ compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
13
+ compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
14
14
  compileExplain(sql: string): string;
15
15
  }
@@ -22,20 +22,21 @@ export class SQLiteGrammar extends Grammar {
22
22
  const limitSql = limit === undefined ? "LIMIT -1 " : "";
23
23
  return `${limitSql}OFFSET ${offset}`;
24
24
  }
25
- compileDateWhere(type, column, operator, value) {
25
+ compileDateWhere(type, column, operator, value, binding) {
26
+ const val = binding ? binding(value) : this.escape(value);
26
27
  switch (type) {
27
28
  case "date":
28
- return `date(${column}) ${operator} ${this.escape(value)}`;
29
+ return `date(${column}) ${operator} ${val}`;
29
30
  case "day":
30
- return `CAST(strftime('%d', ${column}) AS INTEGER) ${operator} ${this.escape(value)}`;
31
+ return `CAST(strftime('%d', ${column}) AS INTEGER) ${operator} ${val}`;
31
32
  case "month":
32
- return `CAST(strftime('%m', ${column}) AS INTEGER) ${operator} ${this.escape(value)}`;
33
+ return `CAST(strftime('%m', ${column}) AS INTEGER) ${operator} ${val}`;
33
34
  case "year":
34
- return `CAST(strftime('%Y', ${column}) AS INTEGER) ${operator} ${this.escape(value)}`;
35
+ return `CAST(strftime('%Y', ${column}) AS INTEGER) ${operator} ${val}`;
35
36
  case "time":
36
- return `time(${column}) ${operator} ${this.escape(value)}`;
37
+ return `time(${column}) ${operator} ${val}`;
37
38
  default:
38
- return `${column} ${operator} ${this.escape(value)}`;
39
+ return `${column} ${operator} ${val}`;
39
40
  }
40
41
  }
41
42
  compileInsertOrIgnore(table, columns, values) {
@@ -47,18 +48,18 @@ export class SQLiteGrammar extends Grammar {
47
48
  .join(", ");
48
49
  return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON CONFLICT(${uniqueBy.map((c) => this.wrap(c)).join(", ")}) DO UPDATE SET ${updateCols}`;
49
50
  }
50
- compileJsonContains(column, value) {
51
- return `${column} IN (SELECT value FROM json_each(${column})) AND ${this.escape(value)} IN (SELECT value FROM json_each(${column}))`;
51
+ compileJsonContains(column, value, binding) {
52
+ return `${column} IN (SELECT value FROM json_each(${column})) AND ${binding ? binding(value) : this.escape(value)} IN (SELECT value FROM json_each(${column}))`;
52
53
  }
53
- compileJsonLength(column, operator, value) {
54
- return `(SELECT COUNT(*) FROM json_each(${column})) ${operator} ${this.escape(value)}`;
54
+ compileJsonLength(column, operator, value, binding) {
55
+ return `(SELECT COUNT(*) FROM json_each(${column})) ${operator} ${binding ? binding(value) : this.escape(value)}`;
55
56
  }
56
- compileRegexp(column, value, not) {
57
+ compileRegexp(column, value, not, binding) {
57
58
  const op = not ? "NOT REGEXP" : "REGEXP";
58
- return `${column} ${op} ${this.escape(value)}`;
59
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
59
60
  }
60
- compileFullText(columns, value) {
61
- return columns.map((c) => `${this.wrap(c)} LIKE ${this.escape(`%${value}%`)}`).join(" OR ");
61
+ compileFullText(columns, value, binding) {
62
+ return columns.map((c) => `${this.wrap(c)} LIKE ${binding ? binding(`%${value}%`) : this.escape(`%${value}%`)}`).join(" OR ");
62
63
  }
63
64
  compileExplain(sql) {
64
65
  return `EXPLAIN QUERY PLAN ${sql}`;
@@ -29,12 +29,15 @@ export interface ForeignKeyDefinition {
29
29
  onUpdate?: string;
30
30
  }
31
31
  export interface WhereClause {
32
- type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists";
32
+ type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
33
33
  column: string;
34
+ columns?: string[];
34
35
  operator?: string;
35
36
  value?: any;
36
37
  boolean: "and" | "or";
37
38
  scope?: string;
39
+ not?: boolean;
40
+ dateType?: string;
38
41
  }
39
42
  export interface OrderClause {
40
43
  column: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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",