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-builder0.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:
- can't specify
foreignKey/onDelete, and - (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
- Normalize
belongsTo/hasMany/hasOneentries before any string op — accept both'Model'and{ model, foreignKey?, onDelete? }(mirror the existingbelongsToManyobject handling). This alone fixes the crash. - Emit the FK constraint from the (now-accepted) object form — inline in
CREATE TABLEfor SQLite (which can'tALTER TABLE ADD CONSTRAINT),ALTER TABLEfor MySQL/Postgres — honoringonDelete. That closes the referential-integrity gap behind thefk-auditwarnings.
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.