Skip to content

Make Insert<> respect .db.auto()/.db.inserted(), and typecheck the public API - #24

Open
brainkim wants to merge 1 commit into
mainfrom
fix/insert-type-auto-fields
Open

Make Insert<> respect .db.auto()/.db.inserted(), and typecheck the public API#24
brainkim wants to merge 1 commit into
mainfrom
fix/insert-type-auto-fields

Conversation

@brainkim

Copy link
Copy Markdown
Member

The bug

The README's own Quick Start does not compile:

const Users = table("users", {
  id: z.string().uuid().db.primary().db.auto(),
  email: z.string().email().db.unique(),
  name: z.string(),
});

const user = await db.insert(Users, {email: "alice@example.com", name: "Alice"});
//                                  ^^^^ Property 'id' is missing in type
//                                       '{ email: string; name: string; }'

The runtime is correct — it generates the UUID. Only TypeScript rejects it. So the headline example of a library whose pitch is "get typed objects" did not typecheck. Same example appears in the getting-started guide and on zendb.org.

Cause

Insert<T> was just z.input<T["schema"]>. The .db.*() metadata lives in a runtime side-channel the type system can't see, so z.input saw id: z.string() → required.

Both JSDoc comments already claimed the correct behaviour:

  • Insert"Infer the insert type (respects defaults and .db.auto() fields)"
  • auto()"Field becomes optional for insert."

Only the types disagreed.

Fix

.db.auto(), .db.inserted() and .db.upserted() now brand their return type with a phantom DBInsertOptional — exactly the mechanism .db.references() already uses with __refTable/__refAs. Insert<T> reads the brand and makes those keys optional.

Because db is typed ZodDBMethods<this>, the brand survives chaining in any order.db.auto().db.primary() and .db.primary().db.auto() both work. Pinned by a test.

Update<T> is already Partial<>, so it needed no change.

Why 597 passing tests missed it

Nothing typechecks the public API. bun test doesn't typecheck, and tsconfig.json included only src/** — so test files and the README were never compiled. A type-level bug in the public surface could not be caught.

This PR closes that hole:

  • test/types/public-api.ts — compile-time tests, including the README Quick Start verbatim, plus @ts-expect-error assertions that required fields and unknown fields are still rejected.
  • npm run typecheck now runs tsconfig.typecheck.json, which includes them.

test/*.test.ts is deliberately still excluded — those files have pre-existing type errors, and folding them in would make the check useless.

Verification

Mutation-tested the guard — reverting the brand on auto() makes the new check fail with the original error, confirming it isn't passing vacuously:

test/types/public-api.ts(69,14): error TS2741:
  Property 'id' is missing in type '{ title: string; }'
  • npm run typecheck — clean (and fails on regression, as above)
  • bun test — 594 pass / 0 fail
  • npm run test:node — passes

🤖 Generated with Claude Code

…blic API

The README's own Quick Start did not compile:

    const user = await db.insert(Users, {email: "alice@...", name: "Alice"});
    //                                  ^ Property 'id' is missing

even though `id` is `.db.primary().db.auto()`. The runtime was correct — it
generated the UUID — but TypeScript rejected the call, so the headline example
of a library whose pitch is "get typed objects" did not typecheck.

Cause: Insert<T> was just `z.input<T["schema"]>`. The .db.*() metadata lives in
a runtime side-channel that the type system cannot see, so `z.input` saw
`id: z.string()` and made it required. Both the Insert JSDoc ("respects defaults
and .db.auto() fields") and auto()'s own JSDoc ("Field becomes optional for
insert") already claimed the behaviour; only the types disagreed.

Fix: .db.auto(), .db.inserted() and .db.upserted() now brand their return type
with a phantom DBInsertOptional, exactly as .db.references() already brands with
__refTable/__refAs. Insert<T> reads the brand and makes those keys optional.
Because `db` is typed as ZodDBMethods<this>, the brand survives further chaining
in any order, so .db.auto().db.primary() and .db.primary().db.auto() both work.

Update<T> is already Partial<>, so it needed no change.

Why 597 passing tests missed this: nothing typechecks the public API. `bun test`
does not typecheck, and tsconfig included only `src/**`, so test files and the
README were never compiled. A type-level bug in the public surface could not be
caught.

So this also closes that hole: test/types/public-api.ts holds compile-time tests
(including the README Quick Start verbatim), and `npm run typecheck` now runs
against tsconfig.typecheck.json which includes them. Verified by mutation:
reverting the brand on auto() makes typecheck fail with the original error.

test/*.test.ts is deliberately still excluded — those files have pre-existing
type errors, and folding them in would make the check useless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Vvtr747kUp1NobqCBeZJi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant