bun-query-builder 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -14728,17 +14728,15 @@ function createQueryBuilder(state) {
14728
14728
  addWhereText("WHERE", `${String(col)} IS ${onlyTrashed ? "NOT " : ""}NULL`);
14729
14729
  }
14730
14730
  }
14731
+ const cacheKey = useCache ? `${String(finalQuery)}\x00${JSON.stringify(whereParams)}` : "";
14731
14732
  if (useCache) {
14732
- const cacheKey = String(finalQuery);
14733
14733
  const cached = queryCache.get(cacheKey);
14734
14734
  if (cached)
14735
14735
  return cached;
14736
14736
  }
14737
14737
  const result = await runWithHooks(finalQuery, "select", { signal: abortSignal, timeoutMs });
14738
- if (useCache) {
14739
- const cacheKey = String(finalQuery);
14738
+ if (useCache)
14740
14739
  queryCache.set(cacheKey, result, cacheTtl);
14741
- }
14742
14740
  return hydratePivotRows(result);
14743
14741
  },
14744
14742
  async executeTakeFirst() {
@@ -23803,6 +23801,13 @@ function assertValidIdentifier(name, context) {
23803
23801
  if (!SAFE_SQL_IDENTIFIER.test(name))
23804
23802
  throw new TypeError(`[bun-query-builder] ${context}: identifier '${name}' contains characters outside [A-Za-z0-9_] \u2014 refusing to interpolate into SQL`);
23805
23803
  }
23804
+ function assertValidOrderByColumn(name, context) {
23805
+ if (typeof name !== "string" || name.length === 0)
23806
+ throw new TypeError(`[bun-query-builder] ${context}: identifier must be a non-empty string, got ${typeof name}`);
23807
+ const parts = name.split(".");
23808
+ if (parts.length > 2 || parts.some((p2) => p2.length === 0 || p2.length > 64 || !SAFE_SQL_IDENTIFIER.test(p2)))
23809
+ throw new TypeError(`[bun-query-builder] ${context}: invalid ORDER BY column '${name}' \u2014 expected 'column' or 'table.column' of [A-Za-z0-9_] \u2014 refusing to interpolate into SQL`);
23810
+ }
23806
23811
  function getModelFromRegistry(name) {
23807
23812
  if (!_getModel) {
23808
23813
  try {
@@ -24091,12 +24096,16 @@ class ModelInstance {
24091
24096
  for (const [key, value] of Object.entries(data)) {
24092
24097
  const attr = attrs[key];
24093
24098
  if (attr?.fillable && !attr?.guarded) {
24099
+ if (this._original === null)
24100
+ this._original = { ...this._attributes };
24094
24101
  this._attributes[key] = value;
24095
24102
  }
24096
24103
  }
24097
24104
  return this;
24098
24105
  }
24099
24106
  forceFill(data) {
24107
+ if (this._original === null)
24108
+ this._original = { ...this._attributes };
24100
24109
  Object.assign(this._attributes, data);
24101
24110
  return this;
24102
24111
  }
@@ -24492,7 +24501,7 @@ class BelongsToManyRelationBuilder {
24492
24501
  return this;
24493
24502
  }
24494
24503
  orderBy(column, direction = "asc") {
24495
- assertValidIdentifier(column, "orderBy(column)");
24504
+ assertValidOrderByColumn(column, "orderBy(column)");
24496
24505
  if (direction !== "asc" && direction !== "desc")
24497
24506
  throw new TypeError(`[bun-query-builder] orderBy(direction): expected 'asc' or 'desc', got '${direction}'`);
24498
24507
  this._orderBy.push(`${column} ${direction.toUpperCase()}`);
@@ -24833,7 +24842,7 @@ class ModelQueryBuilder {
24833
24842
  return this;
24834
24843
  }
24835
24844
  orderBy(column, direction = "asc") {
24836
- assertValidIdentifier(column, "orderBy(column)");
24845
+ assertValidOrderByColumn(column, "orderBy(column)");
24837
24846
  if (direction !== "asc" && direction !== "desc")
24838
24847
  throw new TypeError(`[bun-query-builder] orderBy(direction): expected 'asc' or 'desc', got '${direction}'`);
24839
24848
  this._orderBy.push({ column, direction });
@@ -25194,7 +25203,14 @@ class ModelQueryBuilder {
25194
25203
  sql2 += ` WHERE ${whereBody}`;
25195
25204
  }
25196
25205
  const row = await exec.get(sql2, params);
25197
- return row?.v == null ? null : Number(row.v);
25206
+ const v2 = row?.v;
25207
+ if (v2 == null)
25208
+ return null;
25209
+ if (typeof v2 === "string") {
25210
+ const n2 = Number(v2);
25211
+ return v2.trim() !== "" && !Number.isNaN(n2) ? n2 : v2;
25212
+ }
25213
+ return v2;
25198
25214
  }
25199
25215
  max(column) {
25200
25216
  return this.aggregate("MAX", column);
@@ -25203,10 +25219,10 @@ class ModelQueryBuilder {
25203
25219
  return this.aggregate("MIN", column);
25204
25220
  }
25205
25221
  async avg(column) {
25206
- return await this.aggregate("AVG", column) || 0;
25222
+ return Number(await this.aggregate("AVG", column) ?? 0) || 0;
25207
25223
  }
25208
25224
  async sum(column) {
25209
- return await this.aggregate("SUM", column) || 0;
25225
+ return Number(await this.aggregate("SUM", column) ?? 0) || 0;
25210
25226
  }
25211
25227
  async delete() {
25212
25228
  const exec = getExecutor();
@@ -29920,7 +29936,7 @@ function getPrefix() {
29920
29936
  }
29921
29937
  var prefix = getPrefix();
29922
29938
  // package.json
29923
- var version2 = "0.1.27";
29939
+ var version2 = "0.1.29";
29924
29940
 
29925
29941
  // bin/cli.ts
29926
29942
  init_actions();
package/dist/browser.d.ts CHANGED
@@ -201,9 +201,12 @@ declare type BrowserAttributeKeys<TDef extends BrowserModelDefinition> = keyof T
201
201
  declare type InferBrowserAttributeType<TAttr> = TAttr extends { type: infer T } ? InferType<T> :
202
202
  TAttr extends { factory: (faker: unknown) => infer R } ? R :
203
203
  unknown;
204
- // Build the full attributes type from definition
204
+ // Build the full attributes type from definition. Columns declared
205
+ // `nullable: true` admit null — mirrors InferAttributes in type-inference.ts.
205
206
  declare type InferBrowserModelAttributes<TDef extends BrowserModelDefinition> = {
206
- [K in BrowserAttributeKeys<TDef>]: InferBrowserAttributeType<TDef['attributes'][K]>
207
+ [K in BrowserAttributeKeys<TDef>]: TDef['attributes'][K] extends { nullable: true }
208
+ ? InferBrowserAttributeType<TDef['attributes'][K]> | null
209
+ : InferBrowserAttributeType<TDef['attributes'][K]>
207
210
  }
208
211
  // System fields added by traits
209
212
  declare type BrowserSystemFields<TDef extends BrowserModelDefinition> = { id: number } &
package/dist/client.d.ts CHANGED
@@ -104,8 +104,11 @@ export declare interface BaseSelectQueryBuilder<DB extends DatabaseSchema<any>,
104
104
  having: (expr: WhereExpression<any>) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
105
105
  havingRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
106
106
  addSelect: (...columns: ((keyof DB[TTable]['columns'] & string) | string | SqlFragment)[]) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
107
- select?: (columns: string | SqlFragment | ((keyof DB[TTable]['columns'] & string) | string | SqlFragment)[]) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
108
- selectAll?: () => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
107
+ select?: {
108
+ <K extends keyof DB[TTable]['columns'] & string>(columns: K[]): SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>, TJoined>
109
+ (columns: string | SqlFragment | (string | SqlFragment)[]): SelectQueryBuilder<DB, TTable, TSelected, TJoined>
110
+ }
111
+ selectAll?: () => SelectQueryBuilder<DB, TTable, DB[TTable]['columns'], TJoined>
109
112
  orderByRaw: (fragment: SqlFragment) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
110
113
  union: (other: { toSQL: () => any }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
111
114
  unionAll: (other: { toSQL: () => any }) => SelectQueryBuilder<DB, TTable, TSelected, TJoined>
@@ -146,7 +149,7 @@ export declare interface BaseSelectQueryBuilder<DB extends DatabaseSchema<any>,
146
149
  }
147
150
  exists: () => Promise<boolean>
148
151
  doesntExist: () => Promise<boolean>
149
- cursorPaginate: (perPage: number, cursor?: string | number, column?: string, direction?: 'asc' | 'desc') => Promise<{ data: any[], meta: { perPage: number, nextCursor: string | number | null } }>
152
+ cursorPaginate: (perPage: number, cursor?: string | number, column?: string, direction?: 'asc' | 'desc') => Promise<{ data: SelectedRow<DB, TTable, TSelected>[], meta: { perPage: number, nextCursor: string | number | null } }>
150
153
  chunk: (size: number, handler: (rows: any[]) => Promise<void> | void) => Promise<void>
151
154
  chunkById: (size: number, column?: string, handler?: (rows: any[]) => Promise<void> | void) => Promise<void>
152
155
  eachById: (size: number, column?: string, handler?: (row: any) => Promise<void> | void) => Promise<void>
@@ -175,8 +178,8 @@ export declare interface BaseSelectQueryBuilder<DB extends DatabaseSchema<any>,
175
178
  count: () => Promise<number>
176
179
  avg: (column: keyof DB[TTable]['columns'] & string) => Promise<number>
177
180
  sum: (column: keyof DB[TTable]['columns'] & string) => Promise<number>
178
- max: (column: keyof DB[TTable]['columns'] & string) => Promise<any>
179
- min: (column: keyof DB[TTable]['columns'] & string) => Promise<any>
181
+ max: <K extends keyof DB[TTable]['columns'] & string>(column: K) => Promise<DB[TTable]['columns'][K] | null>
182
+ min: <K extends keyof DB[TTable]['columns'] & string>(column: K) => Promise<DB[TTable]['columns'][K] | null>
180
183
  readonly rows: TSelected[]
181
184
  readonly row: TSelected
182
185
  values: () => Promise<any[][]>
@@ -224,13 +227,19 @@ export declare interface TableQueryBuilder<DB extends DatabaseSchema<any>, TTabl
224
227
  insert: (data: Partial<DB[TTable]['columns']> | Partial<DB[TTable]['columns']>[]) => InsertQueryBuilder<DB, TTable>
225
228
  update: (values: Partial<DB[TTable]['columns']>) => UpdateQueryBuilder<DB, TTable>
226
229
  delete: () => DeleteQueryBuilder<DB, TTable>
227
- select: (...columns: (keyof DB[TTable]['columns'] & string)[]) => SelectQueryBuilder<DB, TTable, any>
230
+ select: <K extends keyof DB[TTable]['columns'] & string>(...columns: K[]) => SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>>
228
231
  }
229
232
  export declare interface QueryBuilder<DB extends DatabaseSchema<any>> {
230
- select: <TTable extends keyof DB & string, K extends keyof DB[TTable]['columns'] & string>(
231
- table: TTable,
232
- ...columns: (K | `${string} as ${string}`)[]
233
- ) => SelectQueryBuilder<DB, TTable, any>
233
+ select: {
234
+ <TTable extends keyof DB & string, K extends keyof DB[TTable]['columns'] & string>(
235
+ table: TTable,
236
+ ...columns: K[]
237
+ ): SelectQueryBuilder<DB, TTable, Pick<DB[TTable]['columns'], K>>
238
+ <TTable extends keyof DB & string>(
239
+ table: TTable,
240
+ ...columns: ((keyof DB[TTable]['columns'] & string) | `${string} as ${string}`)[]
241
+ ): SelectQueryBuilder<DB, TTable, any>
242
+ }
234
243
  selectFrom: <TTable extends keyof DB & string>(table: TTable) => TypedSelectQueryBuilder<DB, TTable, DB[TTable]['columns'], TTable, `SELECT * FROM ${TTable}`>
235
244
  insertInto: <TTable extends keyof DB & string>(table: TTable) => TypedInsertQueryBuilder<DB, TTable>
236
245
  updateTable: <TTable extends keyof DB & string>(table: TTable) => UpdateQueryBuilder<DB, TTable>
@@ -413,22 +422,29 @@ declare type JoinColumn<DB extends DatabaseSchema<any>, TTables extends string>
413
422
  *
414
423
  * The relation names declared for a table, read from the type-level
415
424
  * `relations` map that `DatabaseSchema` carries. Falls back to `string`
416
- * for hand-written schema types that don't declare relation metadata, so
417
- * existing untyped schemas keep compiling.
425
+ * for hand-written schema types that don't declare relation metadata
426
+ * (inferred `R` is `unknown` when the property is absent), so existing
427
+ * untyped schemas keep compiling. A table that declares ZERO relations
428
+ * yields `never` — every relation name is rejected.
418
429
  */
419
430
  export type TableRelationName<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = DB[TTable] extends { relations?: infer R }
420
- ? [keyof NonNullable<R>] extends [never] ? string : keyof NonNullable<R> & string
431
+ ? unknown extends R ? string : keyof NonNullable<R> & string
421
432
  : string;
422
433
  /**
423
434
  * # `WithRelationArg<DB, TTable>`
424
435
  *
425
436
  * Argument accepted by `.with()`: a declared relation name, a dotted nested
426
437
  * path rooted at a declared relation (`'posts.comments'`), or a record
427
- * mapping relation names to constraint callbacks.
438
+ * mapping relation names to constraint callbacks. A table with zero declared
439
+ * relations accepts nothing (the bare `Partial<Record<never, ...>>` would be
440
+ * `{}`, which strings are assignable to — hence the explicit never guard).
428
441
  */
429
- export type WithRelationArg<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = | TableRelationName<DB, TTable>
430
- | `${TableRelationName<DB, TTable>}.${string}`
431
- | Partial<Record<TableRelationName<DB, TTable>, (qb: any) => any>>;
442
+ export type WithRelationArg<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> = [TableRelationName<DB, TTable>] extends [never]
443
+ ? never
444
+ :
445
+ | TableRelationName<DB, TTable>
446
+ | `${TableRelationName<DB, TTable>}.${string}`
447
+ | Partial<Record<TableRelationName<DB, TTable>, (qb: any) => any>>;
432
448
  // Convert snake_case to PascalCase at the type level (e.g. created_at -> CreatedAt)
433
449
  declare type SnakeToPascal<S extends string> = S extends `${infer H}_${infer T}`
434
450
  ? `${Capitalize<H>}${SnakeToPascal<T>}`
@@ -449,11 +465,15 @@ declare type _TypedDynamicWhereMethods<DB extends DatabaseSchema<any>, TTable ex
449
465
  value: DB[TTable]['columns'][K],
450
466
  ) => TypedSelectQueryBuilder<DB, TTable, TSelected, TJoined, `${TSql} AND ${K} = ?`>
451
467
  }
468
+ // NOTE: TypedSelectQueryBuilder must NOT also intersect DynamicWhereMethods —
469
+ // _TypedDynamicWhereMethods declares the same `where<Column>` keys, and the
470
+ // untyped variant (returning a plain SelectQueryBuilder) would win overload
471
+ // resolution, silently downgrading `toSQL()` from the composed literal SQL
472
+ // type back to `string` after any dynamic-where call.
452
473
  export type TypedSelectQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string, TSelected, TJoined extends string = TTable, TSql extends string = `SELECT * FROM ${TTable}`,> = Omit<
453
474
  BaseSelectQueryBuilder<DB, TTable, TSelected, TJoined>,
454
475
  'toSQL' | 'where' | 'andWhere' | 'orWhere' | 'orderBy' | 'limit'
455
- > & DynamicWhereMethods<DB, TTable, TSelected, TJoined>
456
- & _TypedDynamicWhereMethods<DB, TTable, TSelected, TJoined, TSql>
476
+ > & _TypedDynamicWhereMethods<DB, TTable, TSelected, TJoined, TSql>
457
477
  & {
458
478
  toSQL: () => TSql
459
479
  where: (<K extends keyof DB[TTable]['columns'] & string>(
package/dist/orm.d.ts CHANGED
@@ -196,12 +196,17 @@ declare type AttributeKeys<TDef extends ModelDefinition> = keyof TDef['attribute
196
196
  declare type InferAttributeType<TAttr> = TAttr extends { type: infer T } ? InferType<T> :
197
197
  TAttr extends { factory: (faker: Faker) => infer R } ? R :
198
198
  unknown;
199
- // Build the full attributes type from definition
199
+ // Build the full attributes type from definition. Columns declared
200
+ // `nullable: true` admit null — mirrors InferAttributes in type-inference.ts.
200
201
  declare type InferModelAttributes<TDef extends ModelDefinition> = {
201
- [K in AttributeKeys<TDef>]: InferAttributeType<TDef['attributes'][K]>
202
+ [K in AttributeKeys<TDef>]: TDef['attributes'][K] extends { nullable: true }
203
+ ? InferAttributeType<TDef['attributes'][K]> | null
204
+ : InferAttributeType<TDef['attributes'][K]>
202
205
  }
203
- // System fields added by traits
204
- declare type SystemFields<TDef extends ModelDefinition> = { id: number } &
206
+ // System fields added by traits. The primary-key column honors the model's
207
+ // declared `primaryKey` (default 'id') a custom-pk model exposes THAT
208
+ // column, not a phantom 'id'. Mirrors InferAttributes in type-inference.ts.
209
+ declare type SystemFields<TDef extends ModelDefinition> = { [K in TDef extends { primaryKey: infer PK extends string } ? PK : 'id']: number } &
205
210
  (TDef['traits'] extends { useUuid: true } ? { uuid: string } : {}) &
206
211
  (TDef['traits'] extends { useTimestamps: true } ? { created_at: string; updated_at: string | null } : {}) &
207
212
  (TDef['traits'] extends { timestampable: true | object } ? { created_at: string; updated_at: string | null } : {}) &
@@ -213,7 +218,7 @@ declare type SystemFields<TDef extends ModelDefinition> = { id: number } &
213
218
  declare type ModelAttributes<TDef extends ModelDefinition> = InferModelAttributes<TDef> & SystemFields<TDef>;
214
219
  // All valid column names
215
220
  declare type ColumnName<TDef extends ModelDefinition> = | AttributeKeys<TDef>
216
- | 'id'
221
+ | (TDef extends { primaryKey: infer PK extends string } ? PK : 'id')
217
222
  | (TDef['traits'] extends { useUuid: true } ? 'uuid' : never)
218
223
  | (TDef['traits'] extends { useTimestamps: true } ? 'created_at' | 'updated_at' : never)
219
224
  | (TDef['traits'] extends { timestampable: true | object } ? 'created_at' | 'updated_at' : never)
@@ -233,31 +238,26 @@ declare type FillableKeys<TDef extends ModelDefinition> = {
233
238
  declare type NumericColumns<TDef extends ModelDefinition> = {
234
239
  [K in AttributeKeys<TDef>]: TDef['attributes'][K] extends { type: 'number' } ? K : never
235
240
  }[AttributeKeys<TDef>];
236
- // Infer relation names from model definition (supports both array and object syntax)
237
- declare type InferBelongsToNames<TDef> = (TDef extends { belongsTo: readonly (infer R)[] }
238
- ? R extends string ? Lowercase<R> : never : never)
239
- | (TDef extends { belongsTo: Readonly<Record<infer K, unknown>> }
240
- ? K extends string ? K : never : never);
241
- declare type InferHasManyNames<TDef> = (TDef extends { hasMany: readonly (infer R)[] }
242
- ? R extends string ? Lowercase<R> : never : never)
243
- | (TDef extends { hasMany: Readonly<Record<infer K, unknown>> }
244
- ? K extends string ? K : never : never);
245
- declare type InferHasOneNames<TDef> = (TDef extends { hasOne: readonly (infer R)[] }
246
- ? R extends string ? Lowercase<R> : never : never)
247
- | (TDef extends { hasOne: Readonly<Record<infer K, unknown>> }
248
- ? K extends string ? K : never : never);
249
- declare type InferBelongsToManyNames<TDef> = (TDef extends { belongsToMany: readonly (infer R)[] }
250
- ? R extends string ? Lowercase<R> : R extends { model: infer M extends string } ? Lowercase<M> : never : never)
251
- | (TDef extends { belongsToMany: Readonly<Record<infer K, unknown>> }
252
- ? K extends string ? K : never : never);
253
- declare type InferHasOneThroughNames<TDef> = (TDef extends { hasOneThrough: readonly (infer R)[] }
254
- ? R extends string ? Lowercase<R> : R extends { model: infer M extends string } ? Lowercase<M> : never : never)
255
- | (TDef extends { hasOneThrough: Readonly<Record<infer K, unknown>> }
256
- ? K extends string ? K : never : never);
257
- declare type InferHasManyThroughNames<TDef> = (TDef extends { hasManyThrough: readonly (infer R)[] }
258
- ? R extends string ? Lowercase<R> : R extends { model: infer M extends string } ? Lowercase<M> : never : never)
259
- | (TDef extends { hasManyThrough: Readonly<Record<infer K, unknown>> }
260
- ? K extends string ? K : never : never);
241
+ /**
242
+ * Relation names of one relation declaration. Array form lowercases the
243
+ * (unwrapped) model name; record form uses the keys. The array case MUST be
244
+ * checked first: a tuple also structurally matches `Readonly<Record<...>>`
245
+ * and would otherwise leak its own keys ('length', indices, ...) into the
246
+ * relation-name union.
247
+ */
248
+ declare type RelationKeyOf<V> = V extends readonly (infer E)[]
249
+ ? E extends string ? Lowercase<E>
250
+ : E extends { model: infer M extends string } ? Lowercase<M>
251
+ : never
252
+ : V extends Readonly<Record<infer K, unknown>>
253
+ ? K & string
254
+ : never;
255
+ declare type InferBelongsToNames<TDef> = TDef extends { belongsTo: infer V } ? RelationKeyOf<V> : never;
256
+ declare type InferHasManyNames<TDef> = TDef extends { hasMany: infer V } ? RelationKeyOf<V> : never;
257
+ declare type InferHasOneNames<TDef> = TDef extends { hasOne: infer V } ? RelationKeyOf<V> : never;
258
+ declare type InferBelongsToManyNames<TDef> = TDef extends { belongsToMany: infer V } ? RelationKeyOf<V> : never;
259
+ declare type InferHasOneThroughNames<TDef> = TDef extends { hasOneThrough: infer V } ? RelationKeyOf<V> : never;
260
+ declare type InferHasManyThroughNames<TDef> = TDef extends { hasManyThrough: infer V } ? RelationKeyOf<V> : never;
261
261
  export type InferRelationNames<TDef> = | InferBelongsToNames<TDef>
262
262
  | InferHasManyNames<TDef>
263
263
  | InferHasOneNames<TDef>
@@ -317,8 +317,8 @@ declare class ModelInstance<TDef extends ModelDefinition, TSelected extends Colu
317
317
  getAttribute<K extends TSelected>(key: K): K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : unknown;
318
318
  getAttributes(): Pick<ModelAttributes<TDef>, TSelected & keyof ModelAttributes<TDef>>;
319
319
  set<K extends ColumnName<TDef>>(key: K, value: K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : unknown): void;
320
- only<K extends TSelected>(keys: ReadonlyArray<K>): Partial<ModelAttributes<TDef>>;
321
- except<K extends TSelected>(keys: ReadonlyArray<K>): Partial<ModelAttributes<TDef>>;
320
+ only<K extends TSelected>(keys: ReadonlyArray<K>): Pick<ModelAttributes<TDef>, K & keyof ModelAttributes<TDef>>;
321
+ except<K extends TSelected>(keys: ReadonlyArray<K>): Omit<Pick<ModelAttributes<TDef>, TSelected & keyof ModelAttributes<TDef>>, K>;
322
322
  getRelation<R extends InferRelationNames<TDef> & string>(name: R): LoadedRelationValue<TDef, R>;
323
323
  setRelation(name: string, data: ModelInstance<any, any>[] | ModelInstance<any, any> | null): void;
324
324
  getLoadedRelations(): Record<string, ModelInstance<any, any>[] | ModelInstance<any, any> | null>;
@@ -440,8 +440,8 @@ declare class ModelQueryBuilder<TDef extends ModelDefinition, TSelected extends
440
440
  to: number | null
441
441
  }>;
442
442
  pluck<K extends ColumnName<TDef>>(column: K): Promise<(K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : unknown)[]>;
443
- max<K extends ColumnName<TDef>>(column: K): Promise<number | null>;
444
- min<K extends ColumnName<TDef>>(column: K): Promise<number | null>;
443
+ max<K extends ColumnName<TDef>>(column: K): Promise<(K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : number) | null>;
444
+ min<K extends ColumnName<TDef>>(column: K): Promise<(K extends keyof ModelAttributes<TDef> ? ModelAttributes<TDef>[K] : number) | null>;
445
445
  avg<K extends NumericColumns<TDef>>(column: K): Promise<number>;
446
446
  sum<K extends NumericColumns<TDef>>(column: K): Promise<number>;
447
447
  delete(): Promise<number>;
package/dist/src/index.js CHANGED
@@ -14728,17 +14728,15 @@ function createQueryBuilder(state) {
14728
14728
  addWhereText("WHERE", `${String(col)} IS ${onlyTrashed ? "NOT " : ""}NULL`);
14729
14729
  }
14730
14730
  }
14731
+ const cacheKey = useCache ? `${String(finalQuery)}\x00${JSON.stringify(whereParams)}` : "";
14731
14732
  if (useCache) {
14732
- const cacheKey = String(finalQuery);
14733
14733
  const cached = queryCache.get(cacheKey);
14734
14734
  if (cached)
14735
14735
  return cached;
14736
14736
  }
14737
14737
  const result = await runWithHooks(finalQuery, "select", { signal: abortSignal, timeoutMs });
14738
- if (useCache) {
14739
- const cacheKey = String(finalQuery);
14738
+ if (useCache)
14740
14739
  queryCache.set(cacheKey, result, cacheTtl);
14741
- }
14742
14740
  return hydratePivotRows(result);
14743
14741
  },
14744
14742
  async executeTakeFirst() {
@@ -23803,6 +23801,13 @@ function assertValidIdentifier(name, context) {
23803
23801
  if (!SAFE_SQL_IDENTIFIER.test(name))
23804
23802
  throw new TypeError(`[bun-query-builder] ${context}: identifier '${name}' contains characters outside [A-Za-z0-9_] \u2014 refusing to interpolate into SQL`);
23805
23803
  }
23804
+ function assertValidOrderByColumn(name, context) {
23805
+ if (typeof name !== "string" || name.length === 0)
23806
+ throw new TypeError(`[bun-query-builder] ${context}: identifier must be a non-empty string, got ${typeof name}`);
23807
+ const parts = name.split(".");
23808
+ if (parts.length > 2 || parts.some((p2) => p2.length === 0 || p2.length > 64 || !SAFE_SQL_IDENTIFIER.test(p2)))
23809
+ throw new TypeError(`[bun-query-builder] ${context}: invalid ORDER BY column '${name}' \u2014 expected 'column' or 'table.column' of [A-Za-z0-9_] \u2014 refusing to interpolate into SQL`);
23810
+ }
23806
23811
  function getModelFromRegistry(name) {
23807
23812
  if (!_getModel) {
23808
23813
  try {
@@ -24091,12 +24096,16 @@ class ModelInstance {
24091
24096
  for (const [key, value] of Object.entries(data)) {
24092
24097
  const attr = attrs[key];
24093
24098
  if (attr?.fillable && !attr?.guarded) {
24099
+ if (this._original === null)
24100
+ this._original = { ...this._attributes };
24094
24101
  this._attributes[key] = value;
24095
24102
  }
24096
24103
  }
24097
24104
  return this;
24098
24105
  }
24099
24106
  forceFill(data) {
24107
+ if (this._original === null)
24108
+ this._original = { ...this._attributes };
24100
24109
  Object.assign(this._attributes, data);
24101
24110
  return this;
24102
24111
  }
@@ -24492,7 +24501,7 @@ class BelongsToManyRelationBuilder {
24492
24501
  return this;
24493
24502
  }
24494
24503
  orderBy(column, direction = "asc") {
24495
- assertValidIdentifier(column, "orderBy(column)");
24504
+ assertValidOrderByColumn(column, "orderBy(column)");
24496
24505
  if (direction !== "asc" && direction !== "desc")
24497
24506
  throw new TypeError(`[bun-query-builder] orderBy(direction): expected 'asc' or 'desc', got '${direction}'`);
24498
24507
  this._orderBy.push(`${column} ${direction.toUpperCase()}`);
@@ -24833,7 +24842,7 @@ class ModelQueryBuilder {
24833
24842
  return this;
24834
24843
  }
24835
24844
  orderBy(column, direction = "asc") {
24836
- assertValidIdentifier(column, "orderBy(column)");
24845
+ assertValidOrderByColumn(column, "orderBy(column)");
24837
24846
  if (direction !== "asc" && direction !== "desc")
24838
24847
  throw new TypeError(`[bun-query-builder] orderBy(direction): expected 'asc' or 'desc', got '${direction}'`);
24839
24848
  this._orderBy.push({ column, direction });
@@ -25194,7 +25203,14 @@ class ModelQueryBuilder {
25194
25203
  sql2 += ` WHERE ${whereBody}`;
25195
25204
  }
25196
25205
  const row = await exec.get(sql2, params);
25197
- return row?.v == null ? null : Number(row.v);
25206
+ const v2 = row?.v;
25207
+ if (v2 == null)
25208
+ return null;
25209
+ if (typeof v2 === "string") {
25210
+ const n2 = Number(v2);
25211
+ return v2.trim() !== "" && !Number.isNaN(n2) ? n2 : v2;
25212
+ }
25213
+ return v2;
25198
25214
  }
25199
25215
  max(column) {
25200
25216
  return this.aggregate("MAX", column);
@@ -25203,10 +25219,10 @@ class ModelQueryBuilder {
25203
25219
  return this.aggregate("MIN", column);
25204
25220
  }
25205
25221
  async avg(column) {
25206
- return await this.aggregate("AVG", column) || 0;
25222
+ return Number(await this.aggregate("AVG", column) ?? 0) || 0;
25207
25223
  }
25208
25224
  async sum(column) {
25209
- return await this.aggregate("SUM", column) || 0;
25225
+ return Number(await this.aggregate("SUM", column) ?? 0) || 0;
25210
25226
  }
25211
25227
  async delete() {
25212
25228
  const exec = getExecutor();
@@ -302,112 +302,42 @@ export type InferPivotColumns<TModel, R extends string> = ResolveDefinition<TMod
302
302
  : Record<string, unknown>
303
303
  : Record<string, unknown>
304
304
  : Record<string, unknown>;
305
- // ============================================================================
306
- // Internal relation name inference helpers
307
- // ============================================================================
308
- declare type InferBelongsToNames<TDef> = (TDef extends { belongsTo: readonly (infer R)[] }
309
- ? R extends string ? Lowercase<R> : never : never)
310
- | (TDef extends { belongsTo: Readonly<Record<infer K, unknown>> }
311
- ? K extends string ? K : never : never);
312
- declare type InferHasManyNames<TDef> = (TDef extends { hasMany: readonly (infer R)[] }
313
- ? R extends string ? Lowercase<R> : never : never)
314
- | (TDef extends { hasMany: Readonly<Record<infer K, unknown>> }
315
- ? K extends string ? K : never : never);
316
- declare type InferHasOneNames<TDef> = (TDef extends { hasOne: readonly (infer R)[] }
317
- ? R extends string ? Lowercase<R> : never : never)
318
- | (TDef extends { hasOne: Readonly<Record<infer K, unknown>> }
319
- ? K extends string ? K : never : never);
320
- declare type InferBelongsToManyNames<TDef> = (TDef extends { belongsToMany: readonly (infer R)[] }
321
- ? R extends string ? Lowercase<R> : R extends { model: infer M extends string } ? Lowercase<M> : never : never)
322
- | (TDef extends { belongsToMany: Readonly<Record<infer K, unknown>> }
323
- ? K extends string ? K : never : never);
324
- declare type InferHasOneThroughNames<TDef> = (TDef extends { hasOneThrough: readonly (infer R)[] }
325
- ? R extends string ? Lowercase<R> : R extends { model: infer M extends string } ? Lowercase<M> : never : never)
326
- | (TDef extends { hasOneThrough: Readonly<Record<infer K, unknown>> }
327
- ? K extends string ? K : never : never);
328
- declare type InferHasManyThroughNames<TDef> = (TDef extends { hasManyThrough: readonly (infer R)[] }
329
- ? R extends string ? Lowercase<R> : R extends { model: infer M extends string } ? Lowercase<M> : never : never)
330
- | (TDef extends { hasManyThrough: Readonly<Record<infer K, unknown>> }
331
- ? K extends string ? K : never : never);
305
+ /**
306
+ * Relation names of one relation declaration. Array form lowercases the
307
+ * (unwrapped) model name; record form uses the keys. The array case MUST be
308
+ * checked first: a tuple also structurally matches `Readonly<Record<...>>`
309
+ * and would otherwise leak its own keys ('length', indices, ...) into the
310
+ * relation-name union.
311
+ */
312
+ declare type RelationKeyOf<V> = V extends readonly (infer E)[]
313
+ ? E extends string ? Lowercase<E>
314
+ : E extends { model: infer M extends string } ? Lowercase<M>
315
+ : never
316
+ : V extends Readonly<Record<infer K, unknown>>
317
+ ? K & string
318
+ : never;
319
+ declare type InferBelongsToNames<TDef> = TDef extends { belongsTo: infer V } ? RelationKeyOf<V> : never;
320
+ declare type InferHasManyNames<TDef> = TDef extends { hasMany: infer V } ? RelationKeyOf<V> : never;
321
+ declare type InferHasOneNames<TDef> = TDef extends { hasOne: infer V } ? RelationKeyOf<V> : never;
322
+ declare type InferBelongsToManyNames<TDef> = TDef extends { belongsToMany: infer V } ? RelationKeyOf<V> : never;
323
+ declare type InferHasOneThroughNames<TDef> = TDef extends { hasOneThrough: infer V } ? RelationKeyOf<V> : never;
324
+ declare type InferHasManyThroughNames<TDef> = TDef extends { hasManyThrough: infer V } ? RelationKeyOf<V> : never;
332
325
  /**
333
326
  * Determine the cardinality of a relation on a model.
334
- * hasMany 'many', hasOne/belongsTo 'one'
327
+ * hasMany / belongsToMany / hasManyThrough / morphMany / morphToMany /
328
+ * morphedByMany → 'many'; hasOne / belongsTo / hasOneThrough / morphOne →
329
+ * 'one'. Both array and record declaration forms are supported via
330
+ * `RelationKeyOf` (array-first, so tuple keys never leak in).
335
331
  */
336
332
  export type RelationCardinality<TModel, R extends string> = ResolveDefinition<TModel> extends infer TDef
337
- ? // hasMany array syntax
338
- (TDef extends { hasMany: readonly (infer M)[] }
339
- ? Lowercase<M & string> extends R ? 'many' : never
340
- : never)
341
- // hasMany object syntax
342
- | (TDef extends { hasMany: Readonly<Record<infer K, unknown>> }
343
- ? K extends string ? K extends R ? 'many' : never : never
344
- : never)
345
- // hasOne array syntax
346
- | (TDef extends { hasOne: readonly (infer M)[] }
347
- ? Lowercase<M & string> extends R ? 'one' : never
348
- : never)
349
- // hasOne object syntax
350
- | (TDef extends { hasOne: Readonly<Record<infer K, unknown>> }
351
- ? K extends string ? K extends R ? 'one' : never : never
352
- : never)
353
- // belongsTo array syntax
354
- | (TDef extends { belongsTo: readonly (infer M)[] }
355
- ? Lowercase<M & string> extends R ? 'one' : never
356
- : never)
357
- // belongsTo object syntax
358
- | (TDef extends { belongsTo: Readonly<Record<infer K, unknown>> }
359
- ? K extends string ? K extends R ? 'one' : never : never
360
- : never)
361
- // belongsToMany array syntax
362
- | (TDef extends { belongsToMany: readonly (infer M)[] }
363
- ? M extends string
364
- ? Lowercase<M> extends R ? 'many' : never
365
- : M extends { model: infer N extends string }
366
- ? Lowercase<N> extends R ? 'many' : never
367
- : never
368
- : never)
369
- // belongsToMany object syntax
370
- | (TDef extends { belongsToMany: Readonly<Record<infer K, unknown>> }
371
- ? K extends string ? K extends R ? 'many' : never : never
372
- : never)
373
- // hasOneThrough array syntax ({ model } entries)
374
- | (TDef extends { hasOneThrough: readonly (infer M)[] }
375
- ? M extends string
376
- ? Lowercase<M> extends R ? 'one' : never
377
- : M extends { model: infer N extends string }
378
- ? Lowercase<N> extends R ? 'one' : never
379
- : never
380
- : never)
381
- // hasOneThrough object syntax
382
- | (TDef extends { hasOneThrough: Readonly<Record<infer K, unknown>> }
383
- ? K extends string ? K extends R ? 'one' : never : never
384
- : never)
385
- // hasManyThrough array syntax ({ model } entries)
386
- | (TDef extends { hasManyThrough: readonly (infer M)[] }
387
- ? M extends string
388
- ? Lowercase<M> extends R ? 'many' : never
389
- : M extends { model: infer N extends string }
390
- ? Lowercase<N> extends R ? 'many' : never
391
- : never
392
- : never)
393
- // hasManyThrough object syntax
394
- | (TDef extends { hasManyThrough: Readonly<Record<infer K, unknown>> }
395
- ? K extends string ? K extends R ? 'many' : never : never
396
- : never)
397
- // morphOne object syntax
398
- | (TDef extends { morphOne: Readonly<Record<infer K, unknown>> }
399
- ? K extends string ? K extends R ? 'one' : never : never
400
- : never)
401
- // morphMany object syntax
402
- | (TDef extends { morphMany: Readonly<Record<infer K, unknown>> }
403
- ? K extends string ? K extends R ? 'many' : never : never
404
- : never)
405
- // morphToMany object syntax
406
- | (TDef extends { morphToMany: Readonly<Record<infer K, unknown>> }
407
- ? K extends string ? K extends R ? 'many' : never : never
408
- : never)
409
- // morphedByMany object syntax
410
- | (TDef extends { morphedByMany: Readonly<Record<infer K, unknown>> }
411
- ? K extends string ? K extends R ? 'many' : never : never
412
- : never)
333
+ ? (TDef extends { hasMany: infer V } ? (R extends RelationKeyOf<V> ? 'many' : never) : never)
334
+ | (TDef extends { hasOne: infer V } ? (R extends RelationKeyOf<V> ? 'one' : never) : never)
335
+ | (TDef extends { belongsTo: infer V } ? (R extends RelationKeyOf<V> ? 'one' : never) : never)
336
+ | (TDef extends { belongsToMany: infer V } ? (R extends RelationKeyOf<V> ? 'many' : never) : never)
337
+ | (TDef extends { hasOneThrough: infer V } ? (R extends RelationKeyOf<V> ? 'one' : never) : never)
338
+ | (TDef extends { hasManyThrough: infer V } ? (R extends RelationKeyOf<V> ? 'many' : never) : never)
339
+ | (TDef extends { morphOne: infer V } ? (R extends RelationKeyOf<V> ? 'one' : never) : never)
340
+ | (TDef extends { morphMany: infer V } ? (R extends RelationKeyOf<V> ? 'many' : never) : never)
341
+ | (TDef extends { morphToMany: infer V } ? (R extends RelationKeyOf<V> ? 'many' : never) : never)
342
+ | (TDef extends { morphedByMany: infer V } ? (R extends RelationKeyOf<V> ? 'many' : never) : never)
413
343
  : never;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.1.27",
4
+ "version": "0.1.29",
5
5
  "description": "A simple yet performant query builder for TypeScript. Built with Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",