bun-query-builder 0.1.57 → 0.1.59

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
@@ -20018,6 +20018,23 @@ var init_actions = __esm(() => {
20018
20018
  init_wait_ready();
20019
20019
  });
20020
20020
 
20021
+ // src/relation-utils.ts
20022
+ function normalizeRelationEntry(entry) {
20023
+ if (typeof entry === "string")
20024
+ return { model: entry };
20025
+ if (entry && typeof entry === "object" && typeof entry.model === "string") {
20026
+ const e = entry;
20027
+ return { model: e.model, foreignKey: e.foreignKey, onDelete: e.onDelete };
20028
+ }
20029
+ return null;
20030
+ }
20031
+ function normalizeRelationList(rel) {
20032
+ if (!rel)
20033
+ return [];
20034
+ const entries = Array.isArray(rel) ? rel : typeof rel === "object" ? Object.values(rel) : [];
20035
+ return entries.map(normalizeRelationEntry).filter((x) => x !== null);
20036
+ }
20037
+
20021
20038
  // ../../node_modules/@stacksjs/ts-faker/dist/chunk-aggdkeg4.js
20022
20039
  import { existsSync as zS } from "fs";
20023
20040
  import { join as YS } from "path";
@@ -24753,12 +24770,12 @@ class ModelInstance {
24753
24770
  only(keys) {
24754
24771
  const out = {};
24755
24772
  for (const k2 of keys) {
24756
- out[k2] = this._attributes[k2];
24773
+ out[k2] = this._attributes[toSnakeCase(k2)];
24757
24774
  }
24758
24775
  return out;
24759
24776
  }
24760
24777
  except(keys) {
24761
- const drop = new Set(keys.map((k2) => k2));
24778
+ const drop = new Set(keys.map((k2) => toSnakeCase(k2)));
24762
24779
  const out = {};
24763
24780
  for (const k2 of Object.keys(this._attributes)) {
24764
24781
  if (!drop.has(k2))
@@ -24786,7 +24803,8 @@ class ModelInstance {
24786
24803
  if (this._original === null)
24787
24804
  return false;
24788
24805
  if (column) {
24789
- return this._attributes[column] !== this._original[column];
24806
+ const key = toSnakeCase(column);
24807
+ return this._attributes[key] !== this._original[key];
24790
24808
  }
24791
24809
  return Object.keys(this._attributes).some((k2) => this._attributes[k2] !== this._original[k2]);
24792
24810
  }
@@ -24795,7 +24813,7 @@ class ModelInstance {
24795
24813
  }
24796
24814
  getOriginal(column) {
24797
24815
  const orig = this.getOriginalAttributes();
24798
- return orig[column];
24816
+ return orig[toSnakeCase(column)];
24799
24817
  }
24800
24818
  getChanges() {
24801
24819
  if (this._original === null)
@@ -24834,7 +24852,7 @@ class ModelInstance {
24834
24852
  const setters = this._definition.set || {};
24835
24853
  for (const [key, setter] of Object.entries(setters)) {
24836
24854
  if (this.isDirty(key)) {
24837
- this._attributes[key] = setter(this._attributes);
24855
+ this._attributes[toSnakeCase(key)] = setter(this._attributes);
24838
24856
  }
24839
24857
  }
24840
24858
  if (this._attributes[pk]) {
@@ -25045,6 +25063,34 @@ function resolveRelation(definition, relationName) {
25045
25063
  }
25046
25064
  return null;
25047
25065
  }
25066
+ function findBelongsToEntry() {
25067
+ const rel = definition.belongsTo;
25068
+ if (!rel)
25069
+ return null;
25070
+ const lower = relationName.toLowerCase();
25071
+ const unwrap = (entry) => {
25072
+ if (typeof entry === "string")
25073
+ return { model: entry };
25074
+ if (entry && typeof entry === "object" && typeof entry.model === "string")
25075
+ return { model: entry.model, foreignKey: entry.foreignKey };
25076
+ return null;
25077
+ };
25078
+ if (Array.isArray(rel)) {
25079
+ for (const item of rel) {
25080
+ const e2 = unwrap(item);
25081
+ if (e2 && e2.model.toLowerCase() === lower)
25082
+ return e2;
25083
+ }
25084
+ return null;
25085
+ }
25086
+ if (typeof rel === "object") {
25087
+ for (const [key, value] of Object.entries(rel)) {
25088
+ if (key === relationName || key.toLowerCase() === lower)
25089
+ return unwrap(value);
25090
+ }
25091
+ }
25092
+ return null;
25093
+ }
25048
25094
  function findBelongsToManyEntry() {
25049
25095
  const rel = definition.belongsToMany;
25050
25096
  if (!rel)
@@ -25084,12 +25130,13 @@ function resolveRelation(definition, relationName) {
25084
25130
  const foreignKey = toSnakeCase(parentName) + "_id";
25085
25131
  return { type: "hasOne", relatedModelName: hasOneModel, relatedTable, foreignKey, localKey: parentPk };
25086
25132
  }
25087
- const belongsToModel = findModelName(definition.belongsTo);
25088
- if (belongsToModel) {
25133
+ const belongsToEntry = findBelongsToEntry();
25134
+ if (belongsToEntry) {
25135
+ const belongsToModel = belongsToEntry.model;
25089
25136
  const relatedModel = getModelFromRegistry(belongsToModel);
25090
25137
  const relatedTable = relatedModel?.getTable?.() || toTableName(belongsToModel);
25091
25138
  const relatedPk = relatedModel?.getDefinition?.()?.primaryKey || "id";
25092
- const foreignKey = toSnakeCase(belongsToModel) + "_id";
25139
+ const foreignKey = belongsToEntry.foreignKey || `${toSnakeCase(belongsToModel)}_id`;
25093
25140
  return { type: "belongsTo", relatedModelName: belongsToModel, relatedTable, foreignKey, localKey: relatedPk };
25094
25141
  }
25095
25142
  const found = findBelongsToManyEntry();
@@ -25634,7 +25681,10 @@ class ModelQueryBuilder {
25634
25681
  return this;
25635
25682
  }
25636
25683
  with(...relations) {
25637
- this._withRelations = relations;
25684
+ for (const r2 of relations) {
25685
+ if (!this._withRelations.includes(r2))
25686
+ this._withRelations.push(r2);
25687
+ }
25638
25688
  return this;
25639
25689
  }
25640
25690
  getWithRelations() {
@@ -26193,12 +26243,19 @@ function createModel(definition) {
26193
26243
  }
26194
26244
  async function createTableFromModel(definition) {
26195
26245
  const exec = getExecutor();
26196
- const pk = definition.primaryKey || "id";
26246
+ const pk = toSnakeCase(definition.primaryKey || "id");
26197
26247
  const columns = [];
26248
+ const emitted = new Set([pk]);
26198
26249
  columns.push(definition.autoIncrement !== false ? `${pk} INTEGER PRIMARY KEY AUTOINCREMENT` : `${pk} INTEGER PRIMARY KEY`);
26199
- if (definition.traits?.useUuid)
26250
+ if (definition.traits?.useUuid) {
26200
26251
  columns.push("uuid TEXT UNIQUE");
26201
- for (const [name, attr] of Object.entries(definition.attributes)) {
26252
+ emitted.add("uuid");
26253
+ }
26254
+ for (const [attrName, attr] of Object.entries(definition.attributes)) {
26255
+ const name = toSnakeCase(attrName);
26256
+ if (emitted.has(name))
26257
+ continue;
26258
+ emitted.add(name);
26202
26259
  let colType = "TEXT";
26203
26260
  if (attr.type === "number")
26204
26261
  colType = "INTEGER";
@@ -26219,10 +26276,23 @@ async function createTableFromModel(definition) {
26219
26276
  }
26220
26277
  columns.push(colDef);
26221
26278
  }
26279
+ for (const rel of normalizeRelationList(definition.belongsTo)) {
26280
+ const name = rel.foreignKey ?? `${toSnakeCase(rel.model)}_id`;
26281
+ if (emitted.has(name))
26282
+ continue;
26283
+ emitted.add(name);
26284
+ columns.push(`${name} INTEGER`);
26285
+ }
26222
26286
  if (timestampsEnabled(definition)) {
26223
- columns.push("created_at TEXT", "updated_at TEXT");
26287
+ for (const c2 of ["created_at", "updated_at"]) {
26288
+ if (emitted.has(c2))
26289
+ continue;
26290
+ emitted.add(c2);
26291
+ columns.push(`${c2} TEXT`);
26292
+ }
26224
26293
  }
26225
- if (softDeletesEnabled(definition)) {
26294
+ if (softDeletesEnabled(definition) && !emitted.has("deleted_at")) {
26295
+ emitted.add("deleted_at");
26226
26296
  columns.push("deleted_at TEXT");
26227
26297
  }
26228
26298
  await exec.run(`CREATE TABLE IF NOT EXISTS ${definition.table} (${columns.join(", ")})`, []);
@@ -26265,7 +26335,7 @@ async function seedModel(definition, count, faker) {
26265
26335
  const data = {};
26266
26336
  for (const [name, attr] of Object.entries(definition.attributes)) {
26267
26337
  if (attr.factory)
26268
- data[name] = attr.factory(faker);
26338
+ data[toSnakeCase(name)] = attr.factory(faker);
26269
26339
  }
26270
26340
  if (timestampsEnabled(definition)) {
26271
26341
  const now = formatNow();
@@ -27766,23 +27836,6 @@ async function loadModels(options) {
27766
27836
  }
27767
27837
  var init_loader = () => {};
27768
27838
 
27769
- // src/relation-utils.ts
27770
- function normalizeRelationEntry(entry) {
27771
- if (typeof entry === "string")
27772
- return { model: entry };
27773
- if (entry && typeof entry === "object" && typeof entry.model === "string") {
27774
- const e2 = entry;
27775
- return { model: e2.model, foreignKey: e2.foreignKey, onDelete: e2.onDelete };
27776
- }
27777
- return null;
27778
- }
27779
- function normalizeRelationList(rel) {
27780
- if (!rel)
27781
- return [];
27782
- const entries = Array.isArray(rel) ? rel : typeof rel === "object" ? Object.values(rel) : [];
27783
- return entries.map(normalizeRelationEntry).filter((x2) => x2 !== null);
27784
- }
27785
-
27786
27839
  // src/meta.ts
27787
27840
  function buildSchemaMeta(models) {
27788
27841
  const modelToTable = {};
@@ -31022,7 +31075,7 @@ function getPrefix() {
31022
31075
  }
31023
31076
  var prefix = getPrefix();
31024
31077
  // package.json
31025
- var version2 = "0.1.57";
31078
+ var version2 = "0.1.59";
31026
31079
 
31027
31080
  // bin/cli.ts
31028
31081
  init_actions();
package/dist/orm.d.ts CHANGED
@@ -68,6 +68,17 @@ export declare interface ModelHookInstance extends Record<string, unknown> {
68
68
  except?(keys: ReadonlyArray<string>): Record<string, unknown>
69
69
  toArray?(): Record<string, unknown>
70
70
  }
71
+ /**
72
+ * Object form of a `belongsTo` entry. The migration generator has always
73
+ * honored `foreignKey`/`onDelete` here (see `normalizeRelationEntry`), but the
74
+ * declaration type only admitted plain model-name strings — so the supported
75
+ * form was a type error at the call site.
76
+ */
77
+ export declare interface BelongsToEntry {
78
+ readonly model: string
79
+ readonly foreignKey?: string
80
+ readonly onDelete?: import('./schema').OnForeignKeyAction
81
+ }
71
82
  // Base model definition
72
83
  export declare interface ModelDefinition {
73
84
  readonly name: string
@@ -113,7 +124,7 @@ export declare interface ModelDefinition {
113
124
  readonly useActivityLog?: boolean | object
114
125
  readonly useSocials?: readonly string[]
115
126
  }
116
- readonly belongsTo?: readonly string[] | Readonly<Record<string, string>>
127
+ readonly belongsTo?: readonly (string | BelongsToEntry)[] | Readonly<Record<string, string | BelongsToEntry>>
117
128
  readonly hasMany?: readonly string[] | Readonly<Record<string, string>>
118
129
  readonly hasOne?: readonly string[] | Readonly<Record<string, string>>
119
130
  readonly belongsToMany?: readonly (string | object)[] | Readonly<Record<string, string | object>>
@@ -247,7 +258,7 @@ declare type SnakeCaseAttributes<TDef extends ModelDefinition> = {
247
258
  // System fields added by traits. The primary-key column honors the model's
248
259
  // declared `primaryKey` (default 'id') — a custom-pk model exposes THAT
249
260
  // column, not a phantom 'id'. Mirrors InferAttributes in type-inference.ts.
250
- declare type SystemFields<TDef extends ModelDefinition> = { [K in TDef extends { primaryKey: infer PK extends string } ? PK : 'id']: number } &
261
+ declare type TraitSystemFields<TDef extends ModelDefinition> = { [K in TDef extends { primaryKey: infer PK extends string } ? PK : 'id']: number } &
251
262
  (TDef['traits'] extends { useUuid: true } ? { uuid: string } : {}) &
252
263
  (TDef['traits'] extends { useTimestamps: true } ? { created_at: string; updated_at: string | null } : {}) &
253
264
  (TDef['traits'] extends { timestampable: true | object } ? { created_at: string; updated_at: string | null } : {}) &
@@ -255,8 +266,65 @@ declare type SystemFields<TDef extends ModelDefinition> = { [K in TDef extends {
255
266
  (TDef['traits'] extends { softDeletable: true | object } ? { deleted_at: string | null } : {}) &
256
267
  (TDef['traits'] extends { useAuth: true | object } ? { two_factor_secret: string | null; public_key: string | null } : {}) &
257
268
  (TDef['traits'] extends { billable: true | object } ? { stripe_id: string | null } : {});
269
+ /**
270
+ * Every column name the model declares itself, in both the casing it was
271
+ * written in and its snake_case column form.
272
+ */
273
+ declare type DeclaredColumns<TDef extends ModelDefinition> = AttributeKeys<TDef> | SnakeCase<AttributeKeys<TDef>>;
274
+ /**
275
+ * Trait-added system fields, minus anything the model declares explicitly.
276
+ *
277
+ * Without the `Omit`, a model that declares a column a trait also contributes
278
+ * (`created_at: { type: 'date' }` alongside `useTimestamps`, or a `user_id`
279
+ * string alongside `belongsTo: ['User']`) intersected the two shapes into
280
+ * `Date & string` — or outright `never`. Every read of that column then had an
281
+ * uninhabited type and every write was rejected. A column the model spells out
282
+ * is the authority on its own type; the trait default only fills the gap.
283
+ */
284
+ declare type SystemFields<TDef extends ModelDefinition> = Omit<TraitSystemFields<TDef>, DeclaredColumns<TDef>>;
285
+ /**
286
+ * A single `belongsTo` entry reduced to the FK column it puts on this table.
287
+ * Mirrors `normalizeRelationEntry` in relation-utils.ts: an explicit
288
+ * `foreignKey` wins, otherwise the name is derived from the model name.
289
+ */
290
+ declare type BelongsToFkOf<E> = E extends string
291
+ ? `${SnakeCase<Uncapitalize<E>>}_id`
292
+ : E extends { foreignKey: infer F extends string }
293
+ ? F
294
+ : E extends { model: infer M extends string }
295
+ ? `${SnakeCase<Uncapitalize<M>>}_id`
296
+ : never;
297
+ /**
298
+ * The foreign-key columns a `belongsTo` puts on THIS model's table.
299
+ *
300
+ * `belongsTo: ['Farm', 'Field']` means the migration carries `farm_id` and
301
+ * `field_id`, so they are as real as any declared attribute and code queries
302
+ * them constantly (`Mission.where('field_id', field.id)`). They were missing
303
+ * from both `ModelAttributes` and `ColumnName`, which made every such query a
304
+ * type error against a column that demonstrably exists.
305
+ *
306
+ * All four declaration shapes the migration generator accepts are covered:
307
+ * `['Farm']`, `[{ model: 'Farm', foreignKey: 'owner_id' }]`, `{ farm: 'Farm' }`
308
+ * and `{ owner: { model: 'User', foreignKey: 'owner_id' } }`. The array case
309
+ * MUST be checked first — a tuple also structurally matches `Record`, and the
310
+ * record branch would otherwise read its numeric indices as entries.
311
+ *
312
+ * The default name follows the same snake_case convention the relation resolver
313
+ * uses at runtime: `Farm` -> `farm_id`, `TreatmentMap` -> `treatment_map_id`.
314
+ */
315
+ declare type BelongsToKeys<TDef extends ModelDefinition> = TDef extends { belongsTo: infer R }
316
+ ? R extends readonly (infer E)[]
317
+ ? BelongsToFkOf<E>
318
+ : R extends Readonly<Record<string, infer W>>
319
+ ? BelongsToFkOf<W>
320
+ : never
321
+ : never;
322
+ // A declared attribute of the same name wins — see SystemFields above.
323
+ declare type BelongsToColumns<TDef extends ModelDefinition> = {
324
+ [K in Exclude<BelongsToKeys<TDef>, DeclaredColumns<TDef>>]: number
325
+ }
258
326
  // Complete model type
259
- declare type ModelAttributes<TDef extends ModelDefinition> = InferModelAttributes<TDef> & SnakeCaseAttributes<TDef> & SystemFields<TDef>;
327
+ declare type ModelAttributes<TDef extends ModelDefinition> = InferModelAttributes<TDef> & SnakeCaseAttributes<TDef> & SystemFields<TDef> & BelongsToColumns<TDef>;
260
328
  // All valid column names
261
329
  declare type ColumnName<TDef extends ModelDefinition> = | AttributeKeys<TDef>
262
330
  | SnakeCase<AttributeKeys<TDef>>
@@ -267,7 +335,8 @@ declare type ColumnName<TDef extends ModelDefinition> = | AttributeKeys<TDef>
267
335
  | (TDef['traits'] extends { useSoftDeletes: true } ? 'deleted_at' : never)
268
336
  | (TDef['traits'] extends { softDeletable: true | object } ? 'deleted_at' : never)
269
337
  | (TDef['traits'] extends { useAuth: true | object } ? 'two_factor_secret' | 'public_key' : never)
270
- | (TDef['traits'] extends { billable: true | object } ? 'stripe_id' : never);
338
+ | (TDef['traits'] extends { billable: true | object } ? 'stripe_id' : never)
339
+ | BelongsToKeys<TDef>;
271
340
  // Hidden fields
272
341
  declare type HiddenKeys<TDef extends ModelDefinition> = {
273
342
  [K in AttributeKeys<TDef>]: TDef['attributes'][K] extends { hidden: true } ? K : never
@@ -276,9 +345,18 @@ declare type HiddenKeys<TDef extends ModelDefinition> = {
276
345
  declare type FillableKeys<TDef extends ModelDefinition> = {
277
346
  [K in AttributeKeys<TDef>]: TDef['attributes'][K] extends { fillable: true } ? K : never
278
347
  }[AttributeKeys<TDef>];
348
+ /**
349
+ * `belongsTo` foreign keys are always writable.
350
+ *
351
+ * Attaching a record to its parent IS the create: `Mission.create({ …,
352
+ * field_id: field.id })`. There is no attribute declaration to put
353
+ * `fillable: true` on, because the column comes from the relation rather than
354
+ * from `attributes`, so without this every such create was a type error
355
+ * against the one column the relation exists to set.
356
+ */
279
357
  declare type FillableAttributes<TDef extends ModelDefinition> = Partial<Pick<
280
358
  ModelAttributes<TDef>,
281
- FillableKeys<TDef> | SnakeCase<FillableKeys<TDef>>
359
+ FillableKeys<TDef> | SnakeCase<FillableKeys<TDef>> | BelongsToKeys<TDef>
282
360
  >>;
283
361
  // Numeric attribute columns — constrains aggregate methods (sum, avg, etc.)
284
362
  declare type NumericColumns<TDef extends ModelDefinition> = {
package/dist/src/index.js CHANGED
@@ -20017,6 +20017,23 @@ var init_actions = __esm(() => {
20017
20017
  init_wait_ready();
20018
20018
  });
20019
20019
 
20020
+ // src/relation-utils.ts
20021
+ function normalizeRelationEntry(entry) {
20022
+ if (typeof entry === "string")
20023
+ return { model: entry };
20024
+ if (entry && typeof entry === "object" && typeof entry.model === "string") {
20025
+ const e = entry;
20026
+ return { model: e.model, foreignKey: e.foreignKey, onDelete: e.onDelete };
20027
+ }
20028
+ return null;
20029
+ }
20030
+ function normalizeRelationList(rel) {
20031
+ if (!rel)
20032
+ return [];
20033
+ const entries = Array.isArray(rel) ? rel : typeof rel === "object" ? Object.values(rel) : [];
20034
+ return entries.map(normalizeRelationEntry).filter((x) => x !== null);
20035
+ }
20036
+
20020
20037
  // ../../node_modules/@stacksjs/ts-faker/dist/chunk-aggdkeg4.js
20021
20038
  import { existsSync as zS } from "fs";
20022
20039
  import { join as YS } from "path";
@@ -24752,12 +24769,12 @@ class ModelInstance {
24752
24769
  only(keys) {
24753
24770
  const out = {};
24754
24771
  for (const k2 of keys) {
24755
- out[k2] = this._attributes[k2];
24772
+ out[k2] = this._attributes[toSnakeCase(k2)];
24756
24773
  }
24757
24774
  return out;
24758
24775
  }
24759
24776
  except(keys) {
24760
- const drop = new Set(keys.map((k2) => k2));
24777
+ const drop = new Set(keys.map((k2) => toSnakeCase(k2)));
24761
24778
  const out = {};
24762
24779
  for (const k2 of Object.keys(this._attributes)) {
24763
24780
  if (!drop.has(k2))
@@ -24785,7 +24802,8 @@ class ModelInstance {
24785
24802
  if (this._original === null)
24786
24803
  return false;
24787
24804
  if (column) {
24788
- return this._attributes[column] !== this._original[column];
24805
+ const key = toSnakeCase(column);
24806
+ return this._attributes[key] !== this._original[key];
24789
24807
  }
24790
24808
  return Object.keys(this._attributes).some((k2) => this._attributes[k2] !== this._original[k2]);
24791
24809
  }
@@ -24794,7 +24812,7 @@ class ModelInstance {
24794
24812
  }
24795
24813
  getOriginal(column) {
24796
24814
  const orig = this.getOriginalAttributes();
24797
- return orig[column];
24815
+ return orig[toSnakeCase(column)];
24798
24816
  }
24799
24817
  getChanges() {
24800
24818
  if (this._original === null)
@@ -24833,7 +24851,7 @@ class ModelInstance {
24833
24851
  const setters = this._definition.set || {};
24834
24852
  for (const [key, setter] of Object.entries(setters)) {
24835
24853
  if (this.isDirty(key)) {
24836
- this._attributes[key] = setter(this._attributes);
24854
+ this._attributes[toSnakeCase(key)] = setter(this._attributes);
24837
24855
  }
24838
24856
  }
24839
24857
  if (this._attributes[pk]) {
@@ -25044,6 +25062,34 @@ function resolveRelation(definition, relationName) {
25044
25062
  }
25045
25063
  return null;
25046
25064
  }
25065
+ function findBelongsToEntry() {
25066
+ const rel = definition.belongsTo;
25067
+ if (!rel)
25068
+ return null;
25069
+ const lower = relationName.toLowerCase();
25070
+ const unwrap = (entry) => {
25071
+ if (typeof entry === "string")
25072
+ return { model: entry };
25073
+ if (entry && typeof entry === "object" && typeof entry.model === "string")
25074
+ return { model: entry.model, foreignKey: entry.foreignKey };
25075
+ return null;
25076
+ };
25077
+ if (Array.isArray(rel)) {
25078
+ for (const item of rel) {
25079
+ const e2 = unwrap(item);
25080
+ if (e2 && e2.model.toLowerCase() === lower)
25081
+ return e2;
25082
+ }
25083
+ return null;
25084
+ }
25085
+ if (typeof rel === "object") {
25086
+ for (const [key, value] of Object.entries(rel)) {
25087
+ if (key === relationName || key.toLowerCase() === lower)
25088
+ return unwrap(value);
25089
+ }
25090
+ }
25091
+ return null;
25092
+ }
25047
25093
  function findBelongsToManyEntry() {
25048
25094
  const rel = definition.belongsToMany;
25049
25095
  if (!rel)
@@ -25083,12 +25129,13 @@ function resolveRelation(definition, relationName) {
25083
25129
  const foreignKey = toSnakeCase(parentName) + "_id";
25084
25130
  return { type: "hasOne", relatedModelName: hasOneModel, relatedTable, foreignKey, localKey: parentPk };
25085
25131
  }
25086
- const belongsToModel = findModelName(definition.belongsTo);
25087
- if (belongsToModel) {
25132
+ const belongsToEntry = findBelongsToEntry();
25133
+ if (belongsToEntry) {
25134
+ const belongsToModel = belongsToEntry.model;
25088
25135
  const relatedModel = getModelFromRegistry(belongsToModel);
25089
25136
  const relatedTable = relatedModel?.getTable?.() || toTableName(belongsToModel);
25090
25137
  const relatedPk = relatedModel?.getDefinition?.()?.primaryKey || "id";
25091
- const foreignKey = toSnakeCase(belongsToModel) + "_id";
25138
+ const foreignKey = belongsToEntry.foreignKey || `${toSnakeCase(belongsToModel)}_id`;
25092
25139
  return { type: "belongsTo", relatedModelName: belongsToModel, relatedTable, foreignKey, localKey: relatedPk };
25093
25140
  }
25094
25141
  const found = findBelongsToManyEntry();
@@ -25633,7 +25680,10 @@ class ModelQueryBuilder {
25633
25680
  return this;
25634
25681
  }
25635
25682
  with(...relations) {
25636
- this._withRelations = relations;
25683
+ for (const r2 of relations) {
25684
+ if (!this._withRelations.includes(r2))
25685
+ this._withRelations.push(r2);
25686
+ }
25637
25687
  return this;
25638
25688
  }
25639
25689
  getWithRelations() {
@@ -26192,12 +26242,19 @@ function createModel(definition) {
26192
26242
  }
26193
26243
  async function createTableFromModel(definition) {
26194
26244
  const exec = getExecutor();
26195
- const pk = definition.primaryKey || "id";
26245
+ const pk = toSnakeCase(definition.primaryKey || "id");
26196
26246
  const columns = [];
26247
+ const emitted = new Set([pk]);
26197
26248
  columns.push(definition.autoIncrement !== false ? `${pk} INTEGER PRIMARY KEY AUTOINCREMENT` : `${pk} INTEGER PRIMARY KEY`);
26198
- if (definition.traits?.useUuid)
26249
+ if (definition.traits?.useUuid) {
26199
26250
  columns.push("uuid TEXT UNIQUE");
26200
- for (const [name, attr] of Object.entries(definition.attributes)) {
26251
+ emitted.add("uuid");
26252
+ }
26253
+ for (const [attrName, attr] of Object.entries(definition.attributes)) {
26254
+ const name = toSnakeCase(attrName);
26255
+ if (emitted.has(name))
26256
+ continue;
26257
+ emitted.add(name);
26201
26258
  let colType = "TEXT";
26202
26259
  if (attr.type === "number")
26203
26260
  colType = "INTEGER";
@@ -26218,10 +26275,23 @@ async function createTableFromModel(definition) {
26218
26275
  }
26219
26276
  columns.push(colDef);
26220
26277
  }
26278
+ for (const rel of normalizeRelationList(definition.belongsTo)) {
26279
+ const name = rel.foreignKey ?? `${toSnakeCase(rel.model)}_id`;
26280
+ if (emitted.has(name))
26281
+ continue;
26282
+ emitted.add(name);
26283
+ columns.push(`${name} INTEGER`);
26284
+ }
26221
26285
  if (timestampsEnabled(definition)) {
26222
- columns.push("created_at TEXT", "updated_at TEXT");
26286
+ for (const c2 of ["created_at", "updated_at"]) {
26287
+ if (emitted.has(c2))
26288
+ continue;
26289
+ emitted.add(c2);
26290
+ columns.push(`${c2} TEXT`);
26291
+ }
26223
26292
  }
26224
- if (softDeletesEnabled(definition)) {
26293
+ if (softDeletesEnabled(definition) && !emitted.has("deleted_at")) {
26294
+ emitted.add("deleted_at");
26225
26295
  columns.push("deleted_at TEXT");
26226
26296
  }
26227
26297
  await exec.run(`CREATE TABLE IF NOT EXISTS ${definition.table} (${columns.join(", ")})`, []);
@@ -26264,7 +26334,7 @@ async function seedModel(definition, count, faker) {
26264
26334
  const data = {};
26265
26335
  for (const [name, attr] of Object.entries(definition.attributes)) {
26266
26336
  if (attr.factory)
26267
- data[name] = attr.factory(faker);
26337
+ data[toSnakeCase(name)] = attr.factory(faker);
26268
26338
  }
26269
26339
  if (timestampsEnabled(definition)) {
26270
26340
  const now = formatNow();
@@ -27765,23 +27835,6 @@ async function loadModels(options) {
27765
27835
  }
27766
27836
  var init_loader = () => {};
27767
27837
 
27768
- // src/relation-utils.ts
27769
- function normalizeRelationEntry(entry) {
27770
- if (typeof entry === "string")
27771
- return { model: entry };
27772
- if (entry && typeof entry === "object" && typeof entry.model === "string") {
27773
- const e2 = entry;
27774
- return { model: e2.model, foreignKey: e2.foreignKey, onDelete: e2.onDelete };
27775
- }
27776
- return null;
27777
- }
27778
- function normalizeRelationList(rel) {
27779
- if (!rel)
27780
- return [];
27781
- const entries = Array.isArray(rel) ? rel : typeof rel === "object" ? Object.values(rel) : [];
27782
- return entries.map(normalizeRelationEntry).filter((x2) => x2 !== null);
27783
- }
27784
-
27785
27838
  // src/meta.ts
27786
27839
  function buildSchemaMeta(models) {
27787
27840
  const modelToTable = {};
@@ -32,7 +32,7 @@ declare interface InferableModelDefinition {
32
32
  readonly useAuth?: boolean | object
33
33
  readonly billable?: boolean | object
34
34
  }
35
- readonly belongsTo?: readonly string[] | Readonly<Record<string, string>>
35
+ readonly belongsTo?: readonly (string | object)[] | Readonly<Record<string, string | object>>
36
36
  readonly hasMany?: readonly string[] | Readonly<Record<string, string>>
37
37
  readonly hasOne?: readonly string[] | Readonly<Record<string, string>>
38
38
  readonly belongsToMany?: readonly (string | object)[] | Readonly<Record<string, string | object>>
@@ -72,6 +72,41 @@ declare type ResolveDefinition<TModel> = TModel extends { getDefinition: () => i
72
72
  never;
73
73
  /** Extract user-defined attribute keys from a model definition */
74
74
  declare type DefinitionAttributeKeys<TDef extends InferableModelDefinition> = keyof TDef['attributes'] & string;
75
+ /** The model's primary-key column name, defaulting to `id`. */
76
+ declare type PrimaryKeyOf<TDef> = TDef extends { primaryKey: infer PK extends string } ? PK : 'id';
77
+ /**
78
+ * A single `belongsTo` entry reduced to the FK column it puts on this table.
79
+ * Mirrors `normalizeRelationEntry` in relation-utils.ts and `BelongsToFkOf` in
80
+ * orm.ts: an explicit `foreignKey` wins, otherwise the name is derived from
81
+ * the model name.
82
+ */
83
+ declare type BelongsToFkOf<E> = E extends string
84
+ ? `${SnakeCase<Uncapitalize<E>>}_id`
85
+ : E extends { foreignKey: infer F extends string }
86
+ ? F
87
+ : E extends { model: infer M extends string }
88
+ ? `${SnakeCase<Uncapitalize<M>>}_id`
89
+ : never;
90
+ /**
91
+ * The foreign-key columns a `belongsTo` puts on THIS model's table — they are
92
+ * emitted by the migration generator, so they are as real as any declared
93
+ * attribute. The array case MUST be checked first: a tuple also structurally
94
+ * matches `Record`, and the record branch would otherwise read its numeric
95
+ * indices as entries.
96
+ */
97
+ declare type BelongsToKeys<TDef> = TDef extends { belongsTo: infer R }
98
+ ? R extends readonly (infer E)[]
99
+ ? BelongsToFkOf<E>
100
+ : R extends Readonly<Record<string, infer W>>
101
+ ? BelongsToFkOf<W>
102
+ : never
103
+ : never;
104
+ /** camelCase -> snake_case, matching the column names migrations emit. */
105
+ declare type SnakeCase<S extends string> = S extends `${infer C}${infer Rest}`
106
+ ? C extends Lowercase<C>
107
+ ? `${C}${SnakeCase<Rest>}`
108
+ : `_${Lowercase<C>}${SnakeCase<Rest>}`
109
+ : S;
75
110
  // ============================================================================
76
111
  // Single attribute type inference
77
112
  // ============================================================================
@@ -93,14 +128,23 @@ export type InferAttributes<TModel> = ResolveDefinition<TModel> extends infer TD
93
128
  [K in DefinitionAttributeKeys<TDef>]: TDef['attributes'][K] extends { nullable: true }
94
129
  ? InferSingleAttributeType<TDef['attributes'][K]> | null
95
130
  : InferSingleAttributeType<TDef['attributes'][K]>
96
- } & { [K in TDef extends { primaryKey: infer PK extends string } ? PK : 'id']: number }
97
- & (TDef['traits'] extends { useUuid: true } ? { uuid: string } : {})
98
- & (TDef['traits'] extends { useTimestamps: true } ? { created_at: string; updated_at: string | null } : {})
99
- & (TDef['traits'] extends { timestampable: true | object } ? { created_at: string; updated_at: string | null } : {})
100
- & (TDef['traits'] extends { useSoftDeletes: true } ? { deleted_at: string | null } : {})
101
- & (TDef['traits'] extends { softDeletable: true | object } ? { deleted_at: string | null } : {})
102
- & (TDef['traits'] extends { useAuth: true | object } ? { two_factor_secret: string | null; public_key: string | null } : {})
103
- & (TDef['traits'] extends { billable: true | object } ? { stripe_id: string | null } : {})
131
+ }
132
+ // A column the model spells out is the authority on its own type; the
133
+ // trait / FK default only fills the gap. Without the Omit, a model that
134
+ // declares `created_at: { type: 'date' }` alongside `useTimestamps`
135
+ // intersected the two into `Date & string` — an uninhabited type.
136
+ & Omit<
137
+ { [K in PrimaryKeyOf<TDef>]: number }
138
+ & (TDef['traits'] extends { useUuid: true } ? { uuid: string } : {})
139
+ & (TDef['traits'] extends { useTimestamps: true } ? { created_at: string; updated_at: string | null } : {})
140
+ & (TDef['traits'] extends { timestampable: true | object } ? { created_at: string; updated_at: string | null } : {})
141
+ & (TDef['traits'] extends { useSoftDeletes: true } ? { deleted_at: string | null } : {})
142
+ & (TDef['traits'] extends { softDeletable: true | object } ? { deleted_at: string | null } : {})
143
+ & (TDef['traits'] extends { useAuth: true | object } ? { two_factor_secret: string | null; public_key: string | null } : {})
144
+ & (TDef['traits'] extends { billable: true | object } ? { stripe_id: string | null } : {})
145
+ & { [K in BelongsToKeys<TDef>]: number },
146
+ DefinitionAttributeKeys<TDef>
147
+ >
104
148
  : never;
105
149
  /**
106
150
  * The full row type for a model, including an index signature for trait-added
@@ -144,6 +188,11 @@ export type ModelCreateDataLoose<TModel> = InferFillableAttributes<TModel> & { [
144
188
  * Infer only the fillable fields from a model definition or wrapped model.
145
189
  * This is the type accepted by `create()`, `update()`, and `fill()`.
146
190
  *
191
+ * Mirrors `InferAttributes`: a column declared `nullable: true` admits `null`.
192
+ * Columns that are `nullable` or carry a `default` are also OPTIONAL — the
193
+ * database supplies a value when the caller omits one, so requiring them at
194
+ * the call site rejected perfectly valid inserts.
195
+ *
147
196
  * @example
148
197
  * ```ts
149
198
  * type UserFillable = InferFillableAttributes<typeof UserModel>
@@ -151,11 +200,25 @@ export type ModelCreateDataLoose<TModel> = InferFillableAttributes<TModel> & { [
151
200
  * ```
152
201
  */
153
202
  export type InferFillableAttributes<TModel> = ResolveDefinition<TModel> extends infer TDef extends InferableModelDefinition
154
- ? {
155
- [K in DefinitionAttributeKeys<TDef> as TDef['attributes'][K] extends { fillable: true } ? K : never]:
156
- InferSingleAttributeType<TDef['attributes'][K]>
157
- }
203
+ ? FillableRequired<TDef> & FillableOptional<TDef>
158
204
  : never;
205
+ /** Fillable keys the caller MUST supply — no `nullable`, no `default`. */
206
+ declare type FillableRequired<TDef extends InferableModelDefinition> = {
207
+ [K in DefinitionAttributeKeys<TDef> as TDef['attributes'][K] extends { fillable: true }
208
+ ? TDef['attributes'][K] extends { nullable: true } | { default: unknown } ? never : K
209
+ : never]:
210
+ InferFillableType<TDef['attributes'][K]>
211
+ }
212
+ /** Fillable keys the database can fill in — `nullable` or `default`. */
213
+ declare type FillableOptional<TDef extends InferableModelDefinition> = {
214
+ [K in DefinitionAttributeKeys<TDef> as TDef['attributes'][K] extends { fillable: true }
215
+ ? TDef['attributes'][K] extends { nullable: true } | { default: unknown } ? K : never
216
+ : never]?:
217
+ InferFillableType<TDef['attributes'][K]>
218
+ }
219
+ declare type InferFillableType<TAttr> = TAttr extends { nullable: true }
220
+ ? InferSingleAttributeType<TAttr> | null
221
+ : InferSingleAttributeType<TAttr>;
159
222
  /**
160
223
  * Infer the primary key type from a model definition or wrapped model.
161
224
  * Returns the literal string type of the primary key column name.
@@ -183,7 +246,9 @@ export type InferTableName<TModel> = ResolveDefinition<TModel> extends infer TDe
183
246
  : never;
184
247
  /**
185
248
  * Infer all valid relation names from a model definition or wrapped model.
186
- * Combines belongsTo, hasMany, hasOne, belongsToMany, hasOneThrough, and hasManyThrough.
249
+ * Covers every relation kind `RelationCardinality` knows about: belongsTo,
250
+ * hasMany, hasOne, belongsToMany, hasOneThrough, hasManyThrough, and the
251
+ * polymorphic morphOne / morphMany / morphToMany / morphedByMany.
187
252
  *
188
253
  * @example
189
254
  * ```ts
@@ -198,6 +263,10 @@ export type InferRelationNames<TModel> = ResolveDefinition<TModel> extends infer
198
263
  | InferBelongsToManyNames<TDef>
199
264
  | InferHasOneThroughNames<TDef>
200
265
  | InferHasManyThroughNames<TDef>
266
+ | InferMorphOneNames<TDef>
267
+ | InferMorphManyNames<TDef>
268
+ | InferMorphToManyNames<TDef>
269
+ | InferMorphedByManyNames<TDef>
201
270
  : never;
202
271
  /**
203
272
  * Infer column names that have numeric types from a model definition or wrapped model.
@@ -223,10 +292,18 @@ export type InferNumericColumns<TModel> = ResolveDefinition<TModel> extends infe
223
292
  * type UserCols = InferColumnNames<typeof UserModel>
224
293
  * // 'name' | 'email' | 'age' | 'role' | 'id' | 'uuid' | 'created_at' | 'updated_at'
225
294
  * ```
295
+ *
296
+ * Kept in parity with the ORM layer's `ColumnName`: the primary key honors a
297
+ * custom `primaryKey` (a custom-pk model exposes THAT column, not a phantom
298
+ * `id`), belongsTo-implied foreign keys are included because the migration
299
+ * generator emits them, and every declared attribute is also valid in its
300
+ * snake_case column form.
226
301
  */
227
302
  export type InferColumnNames<TModel> = ResolveDefinition<TModel> extends infer TDef extends InferableModelDefinition
228
303
  ? DefinitionAttributeKeys<TDef>
229
- | 'id'
304
+ | SnakeCase<DefinitionAttributeKeys<TDef>>
305
+ | PrimaryKeyOf<TDef>
306
+ | BelongsToKeys<TDef>
230
307
  | (TDef['traits'] extends { useUuid: true } ? 'uuid' : never)
231
308
  | (TDef['traits'] extends { useTimestamps: true } ? 'created_at' | 'updated_at' : never)
232
309
  | (TDef['traits'] extends { timestampable: true | object } ? 'created_at' | 'updated_at' : never)
@@ -303,25 +380,35 @@ export type InferPivotColumns<TModel, R extends string> = ResolveDefinition<TMod
303
380
  : Record<string, unknown>
304
381
  : Record<string, unknown>;
305
382
  /**
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<...>>`
383
+ * Relation names of one relation declaration. A bare string (`morphOne:
384
+ * 'Image'`) lowercases the model name; array form lowercases the (unwrapped)
385
+ * model name; record form uses the keys.
386
+ *
387
+ * Order matters. The bare-string case must precede the record case (a string
388
+ * would otherwise fall through to `never`), and the array case MUST precede
389
+ * the record case: a tuple also structurally matches `Readonly<Record<...>>`
309
390
  * and would otherwise leak its own keys ('length', indices, ...) into the
310
391
  * relation-name union.
311
392
  */
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;
393
+ declare type RelationKeyOf<V> = V extends string
394
+ ? Lowercase<V>
395
+ : V extends readonly (infer E)[]
396
+ ? E extends string ? Lowercase<E>
397
+ : E extends { model: infer M extends string } ? Lowercase<M>
398
+ : never
399
+ : V extends Readonly<Record<infer K, unknown>>
400
+ ? K & string
401
+ : never;
319
402
  declare type InferBelongsToNames<TDef> = TDef extends { belongsTo: infer V } ? RelationKeyOf<V> : never;
320
403
  declare type InferHasManyNames<TDef> = TDef extends { hasMany: infer V } ? RelationKeyOf<V> : never;
321
404
  declare type InferHasOneNames<TDef> = TDef extends { hasOne: infer V } ? RelationKeyOf<V> : never;
322
405
  declare type InferBelongsToManyNames<TDef> = TDef extends { belongsToMany: infer V } ? RelationKeyOf<V> : never;
323
406
  declare type InferHasOneThroughNames<TDef> = TDef extends { hasOneThrough: infer V } ? RelationKeyOf<V> : never;
324
407
  declare type InferHasManyThroughNames<TDef> = TDef extends { hasManyThrough: infer V } ? RelationKeyOf<V> : never;
408
+ declare type InferMorphOneNames<TDef> = TDef extends { morphOne: infer V } ? RelationKeyOf<V> : never;
409
+ declare type InferMorphManyNames<TDef> = TDef extends { morphMany: infer V } ? RelationKeyOf<V> : never;
410
+ declare type InferMorphToManyNames<TDef> = TDef extends { morphToMany: infer V } ? RelationKeyOf<V> : never;
411
+ declare type InferMorphedByManyNames<TDef> = TDef extends { morphedByMany: infer V } ? RelationKeyOf<V> : never;
325
412
  /**
326
413
  * Determine the cardinality of a relation on a model.
327
414
  * hasMany / belongsToMany / hasManyThrough / morphMany / morphToMany /
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.1.57",
4
+ "version": "0.1.59",
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",