Docs / Upgrading

Upgrading

Version-to-version migration guides for Hopak.js.

Each minor release of @hopak/core bumps the public surface in a controlled way. This page collects the migration steps — latest first. Pick the section that matches the version you are on today.

If you are starting fresh, skip this page entirely and follow the Quick start.

0.4.x to 0.5

@hopak/core@0.5.0 reshapes the Database escape hatches. .model() still covers the 90% typed path; the other two slots are now sharper:

The escape hierarchy is now flat:

model()          ← 90% of queries

db.sql`...`      ← raw SQL with types (≈5% escape)

db.builder()     ← Drizzle query builder (<1% escape)

Migration steps

  1. Rename db.raw()db.builder(). Same return type, no behaviour change. Search-and-replace is safe.

    // before
    const drizzle = db.raw() as BunSQLiteDatabase;
    
    // after
    const drizzle = db.builder() as BunSQLiteDatabase;
  2. Prefer db.sql over db.execute(...) for new code. The forwarder stays, so existing calls compile unchanged, but new work should adopt the tagged template.

    // before
    await db.execute('UPDATE post SET published = ? WHERE id = ?', [true, id]);
    
    // after
    await db.sql`UPDATE post SET published = ${true} WHERE id = ${id}`;
    
    // for reads (new capability)
    const rows = await db.sql<{ day: string; n: number }>`
      SELECT DATE(created_at) AS day, COUNT(*) AS n
      FROM post
      WHERE published = true
      GROUP BY day
      ORDER BY day DESC
    `;
  3. Migration files: prefer ctx.sql. hopak migrate new now scaffolds ctx.sql by default. Existing migrations using ctx.execute(...) keep running untouched — no migration rewrite is needed before 0.6.0.

    // new scaffolded migrations
    export async function up(ctx: MigrationContext): Promise<void> {
      await ctx.sql`ALTER TABLE post ADD COLUMN views INTEGER NOT NULL DEFAULT 0`;
    }

What changed under the hood

db.raw() always returned the dialect’s Drizzle client typed as unknown. Callers cast it to { all?, execute? } and branched per dialect to run a single SELECT — the framework’s own tracker.ts did this for the _hopak_migrations read. The new db.sql is driver-native: a pure tag-to-SQL compiler turns ${value} interpolations into dialect-appropriate placeholders (? or $N), then the statement goes straight to bun:sqlite.prepare().all(), postgres.js sql.unsafe(), or mysql2.pool.execute() — no Drizzle on the read path. Inside a transaction, Postgres and MySQL still route through Drizzle’s tx handle (the native connection isn’t reachable from there), but the shape is stable within a Drizzle major and covered by tests.

The rename from raw to builder is cosmetic but clarifies intent: .builder() returns a builder, not a blob of raw SQL capability. .sql is the raw-SQL primitive now.


0.3.x to 0.4

@hopak/core@0.4.0 swaps the validation runtime from Zod to Valibot — ~10× smaller bundle, ~2–3× faster parse, same validate() / buildModelSchema() API. Code using only Hopak’s model-driven validation keeps working untouched. Breaking only for projects that imported Zod directly in route files.

What actually changes

To adopt

bun add valibot in your project, rewrite any hand-rolled schemas in route files using v.* primitives. Model schemas built via buildModelSchema(model) need no changes.

What changed under the hood

Valibot is a tree-shakeable, pipe-based validator; every rule is a standalone function you compose with v.pipe. That’s why the bundle shrinks: if you only use v.string, that’s all you ship. The API shape differs from Zod (z.string().min(3)v.pipe(v.string(), v.minLength(3))) but the semantics line up. See Validation and Errors for the current error shape.


0.2.x to 0.3

@hopak/core@0.3.0 adds migrations. hopak sync still works for greenfield projects, but its role shifts — it’s now the dev bootstrap path, not the schema-evolution path. Non-breaking if you don’t adopt migrations; once you do, the rules below apply.

What changes if you’re on 0.2.x

To adopt migrations on an existing 0.2.x project

hopak migrate init       # captures the current model state
hopak migrate up         # writes _hopak_migrations; no schema change

From here, every model change starts with hopak migrate new <name>.

Nothing breaks if you don’t want migrations yet. Keep using hopak sync until schema evolution (adding a column to an existing table) forces the move. See Migrations for the full walkthrough.

What changed under the hood

Before 0.3, schema state was computed at boot: models were the source of truth and db.sync() coerced the database to match. That’s great for the first week and painful by the second — there’s no history and no way to roll back a column rename. 0.3 introduces an explicit ledger (_hopak_migrations) plus per-migration up/down functions. Sync still owns the zero-to-one moment; migrations own every step after.


0.1.x to 0.2

@hopak/core@0.2.0 is a breaking release. Nothing materializes at runtime from a declaration any more — CRUD endpoints and dev certs are scaffolded by the CLI, and the runtime just executes whatever is in your files.

Migration steps

  1. Upgrade the CLI: bun add -g @hopak/cli@latest.
  2. Upgrade the framework: bun add @hopak/core@latest @hopak/testing@latest in your project.
  3. For every model that had { crud: true }, run once:
    hopak generate crud <model-name>
    That writes app/routes/api/<plural>.ts and app/routes/api/<plural>/[id].ts with the same six verbs the runtime used to inject. Remove { crud: true } from the model file (it’s just a type error now, no behavioral effect):
    // before
    export default model('post', { ... }, { crud: true });
    // after
    export default model('post', { ... });
  4. If you had server.https.enabled: true, run hopak generate cert once. Boot no longer calls openssl behind your back — if the cert files aren’t there, hopak dev fails fast with a pointer to this command.
  5. If you used @hopak/testing’s createTestServer({ withCrud: true }), switch to wiring routes via the new crud.* helpers (or pass rootDir to test the project end-to-end). See @hopak/testing’s README.

Also removed

These existed on the type but were never wired to anything — deleting is mechanical:

Nothing else in the public surface changed — models, relations, query ergonomics, validation, serialization, errors, HTTPS / CORS config, hopak use, hopak sync, hopak check all behave exactly as before.

What changed under the hood

0.2.0 moved CRUD from a runtime feature to a scaffolded one. Instead of the framework synthesizing route handlers from model options at boot, the CLI writes real .ts files you can read, edit, and extend. Same for dev HTTPS certs: explicit files on disk, no implicit openssl invocations. The runtime shrinks; what you see in your tree is what runs.

See CRUD and HTTPS for the current shape.