ReviewOS

stacks/bun-query-builder

Migration generator crashes on object-form belongsTo/hasMany/hasOne (str.replace is not a function)

#1023
Closed glennmichael123 opened this 22 days ago · 0 comments
22 days ago

Summary

generateMigration crashes with str.replace is not a function when a model declares a belongsTo (or hasMany / hasOne) relationship in object form ({ model, foreignKey }). Only the string form (belongsTo: ['Post']) survives generation.

Notably, belongsToMany does accept object form ({ model, pivotTable, firstForeignKey, secondForeignKey }) and works fine — so object-form handling is inconsistent across relationship types. The practical effect is that you cannot declare any foreign-key behaviour (foreignKey, onDelete) on a belongsTo/hasMany/hasOne relationship.

Environment

  • bun-query-builder 0.1.21
  • Reproduced via the SQLite dialect, but the crash is during model → DDL generation (model-name resolution), before any dialect-specific step.

Reproduction

Give a model an object-form belongsTo entry:

export default {
  name: 'ReviewPhoto',
  table: 'review_photos',
  attributes: { /* … */ },

  // ✗ crashes the migration generator:
  belongsTo: [{ model: 'JudgeReview', foreignKey: 'judge_review_id' }, 'User'],

  // ✓ works — but then no FK config (foreignKey / onDelete) is possible:
  // belongsTo: ['JudgeReview', 'User'],
}

Run migration generation (generateMigration(modelsDir, { dialect })):

Migration generation failed: str.replace is not a function.
  (In 'str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")', 'str.replace' is undefined)

Root cause

The snake_case helper (the /([A-Z]+)([A-Z][a-z])/g, '$1_$2' conversion in dist/src/index.js) is handed the relationship entry directly. For object-form entries it receives { model, foreignKey } instead of the model-name string, so str.replace is undefined. The resolver needs to unwrap the entry (typeof e === 'string' ? e : e.model) before snake-casing — the same normalization belongsToMany already performs.

Impact

Because object form crashes, models are forced onto the bare string form, which:

  1. can't specify foreignKey / onDelete, and
  2. (related) generation emits the FK column (judge_review_id) but no FK constraint — so there's no DB-level referential integrity. A sibling auditor (Stacks' fk-audit) then flags every declared relationship as a "missing foreign key" (85 / 85 in our app), which is how we found this.

Suggested fix

  1. Normalize belongsTo / hasMany / hasOne entries before any string op — accept both 'Model' and { model, foreignKey?, onDelete? } (mirror the existing belongsToMany object handling). This alone fixes the crash.
  2. Emit the FK constraint from the (now-accepted) object form — inline in CREATE TABLE for SQLite (which can't ALTER TABLE ADD CONSTRAINT), ALTER TABLE for MySQL/Postgres — honoring onDelete. That closes the referential-integrity gap behind the fk-audit warnings.

Discovered through Stacks: @stacksjs/database's generateMigrations delegates to qbGenerateMigration(modelsDir, { dialect }) and rethrows; the failing str.replace and the snake_case regex are in bun-query-builder itself.

Sign in to comment on this issue.