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 2 months ago ·
2 months 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.

9 days ago

Fixed in 0b455b2 (on main; will land in the next patch release).

normalizeBelongsTo() returned the relation array verbatim, so an object-form entry ({ model, foreignKey }) reached the snake_case helper instead of a model-name string → str.replace is not a function.

  • normalizeBelongsTo() now unwraps every supported shape (string array, object-in-array, record name→Model, record name→config) into { model, foreignKey?, onDelete? } descriptors before any string op — mirroring belongsToMany.
  • The belongsTo FK-column loop honors the object form: a declared foreignKey becomes the column name (fallback ${snake(model)}_id), and onDelete is threaded into the emitted constraint (inline REFERENCES … ON DELETE … for SQLite, ALTER TABLE for MySQL/Postgres) — closing the referential-integrity gap behind the fk-audit warnings (suggested fix #2).
  • buildSchemaMeta's toRecord() hardened the same way (object entries were becoming [object Object] keys).

Verified: object-form hasMany/hasOne also no longer crash; regression tests added (mixed object/string belongsTo → custom FK column + ON DELETE CASCADE in DDL; record-form with object values → ON DELETE SET NULL). Full suite green, typecheck clean.

Sign in to comment on this issue.