# @metaobjectsdev/codegen-ts

> TypeScript codegen engine for MetaObjects — emits Drizzle, Zod, and Fastify artifacts.

Latest version **1.0.9** (published 2026-09-26) · Apache-2.0 license · 3.0K weekly downloads

## Install

```sh
npm install @metaobjectsdev/codegen-ts
pnpm add @metaobjectsdev/codegen-ts
yarn add @metaobjectsdev/codegen-ts
bun add @metaobjectsdev/codegen-ts
```

## Health

**Score 80/100 (A)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; growing popularity.

## Facts

| | |
|---|---|
| Version | 1.0.9 |
| Published | 2026-09-26 |
| First published | 2026-05-23 |
| Weekly downloads | 3.0K |
| License | Apache-2.0 |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 6 |
| Unpacked size | 3.6 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 0 |
| Author | Doug Mealing <doug@dougmealing.com> |
| Maintainers | dougmealing |
| Keywords | metaobjects, codegen, drizzle, zod, fastify, typescript |

## Links

- npm: https://www.npmjs.com/package/@metaobjectsdev/codegen-ts
- Repository: https://github.com/metaobjectsdev/metaobjects
- Homepage: https://metaobjects.dev
- Issues: https://github.com/metaobjectsdev/metaobjects/issues
- npm.io page: https://npm.io/package/@metaobjectsdev/codegen-ts

## Dependencies (6)

- [ts-poet](https://npm.io/package/ts-poet.md) ^6.10.0
- [@biomejs/js-api](https://npm.io/package/@biomejs/js-api.md) ^0.7.0
- [@toon-format/toon](https://npm.io/package/@toon-format/toon.md) ^2.3.0
- [@biomejs/wasm-nodejs](https://npm.io/package/@biomejs/wasm-nodejs.md) ^1.9.4
- [@metaobjectsdev/render](https://npm.io/package/@metaobjectsdev/render.md) 1.0.9
- [@metaobjectsdev/metadata](https://npm.io/package/@metaobjectsdev/metadata.md) 1.0.9

## Alternatives

- [@openai/codex-sdk](https://npm.io/package/@openai/codex-sdk.md) — 731.4K weekly downloads
- [babel-plugin-transform-react-jsx](https://npm.io/package/babel-plugin-transform-react-jsx.md) — 565.0K weekly downloads
- [babel-helper-remove-or-void](https://npm.io/package/babel-helper-remove-or-void.md) — 508.5K weekly downloads
- [@pnpm/store-controller-types](https://npm.io/package/@pnpm/store-controller-types.md) — 186.9K weekly downloads
- [react-native-signature-canvas](https://npm.io/package/react-native-signature-canvas.md) — 155.6K weekly downloads

## Recent versions

- 1.0.9 (latest) — 2026-09-26
- 1.0.9-rc.6 (next) — 2026-09-26
- 1.0.9-rc.5 — 2026-09-26
- 1.0.9-rc.4 — 2026-09-26
- 1.0.9-rc.3 — 2026-09-26
- 1.0.9-rc.2 — 2026-09-26
- 1.0.9-rc.1 — 2026-09-26
- 1.0.8 — 2026-09-26
- 1.0.8-rc.1 — 2026-09-26
- 1.0.7 — 2026-09-24
- 1.0.7-rc.1 — 2026-09-24
- 1.0.5 — 2026-09-23
- 1.0.5-rc.11 — 2026-09-23
- 1.0.5-rc.10 — 2026-09-22
- 1.0.5-rc.9 — 2026-09-22
- … 176 more at https://npm.io/package/@metaobjectsdev/codegen-ts/versions

## README

# @metaobjectsdev/codegen-ts

TypeScript codegen for the metaobjects metamodel — emits Drizzle schema +
inferred types + Zod validators + typed CRUD queries from a loaded `MetaData`.

## Install

```bash
npm install @metaobjectsdev/codegen-ts @metaobjectsdev/metadata drizzle-orm zod
```

## Usage

```typescript
import { MetaDataLoader } from "@metaobjectsdev/metadata";
import { FileSource } from "@metaobjectsdev/metadata/core";
import { generate } from "@metaobjectsdev/codegen-ts";

const { root } = await new MetaDataLoader().load([
  new FileSource("metaobjects/meta.blog.json"),
]);

const result = await generate({
  metadata: root,
  outDir: "./src/db/entities",
  dialect: "sqlite",
  dbImport: "~/server/db",  // path to your { db } export
});

for (const f of result.files) console.log(f.status, f.path);
```

## Output

Per entity, codegen emits **two files**:

- `<Entity>.ts` — Drizzle table definition (with FK `.references()` + `relations()` blocks
  auto-emitted from metadata relationships) + Drizzle-inferred types + Zod validators
- `<Entity>.queries.ts` — typed CRUD query functions
  (`findPostById`, `listPosts`, `createPost`, `updatePost`, `deletePostById`)

Plus a barrel `index.ts` re-exporting from each `<Entity>.ts`.

```typescript
// Use the entity types and table:
import { posts, type Post, PostInsertSchema } from './db/entities/Post.js';

// Use the typed CRUD queries:
import { findPostById, createPost } from './db/entities/Post.queries.js';

const post = await findPostById(42);
const newPost = await createPost({ title: 'Hello', body: 'World', authorId: 1 });
```

Generated files carry an `@generated by @metaobjectsdev/codegen-ts` header. On this
port the header is **informational** — the write decision never reads it. What decides
is `.metaobjects/.gen-state/`: the snapshot body if this machine has one (three-way
merge), otherwise the committed `.hashes.json` (byte-for-byte what we wrote ⇒
overwrite; anything else ⇒ refused, path named, exit code 1). Deleting the header does
not take ownership of a file here — it just changes the content, so the hash stops
matching and the file is refused like any other edit. (The JVM ports are the opposite:
there the marker *is* the decision. See
[`docs/features/own-your-codegen.md`](../../../../docs/features/own-your-codegen.md).)

Hand-written code that metadata can't express — custom queries, derived-column indexes —
goes in a **sibling module you create and import yourself**, conventionally
`<Entity>.extra.ts` beside the generated output. The name is a convention, not a
mechanism: the file is safe because codegen only ever writes the paths recorded in
`.gen-state/.hashes.json`, and for the same reason the generated barrel — built from the
model, not from a directory listing — does **not** re-export it. Import it directly.

### Allowlists opt-in

By default, every generated `<Entity>.ts` file emits a `<Entity>FilterAllowlist`
and `<Entity>SortAllowlist` block, which type-only-imports `FilterAllowlist` /
`SortAllowlist` from `@metaobjectsdev/runtime-ts/drizzle-fastify`. These power
the Fastify-flavored CRUD routes emitted by `routesFile()`.

Worker / Lambda / edge consumers that don't mount Fastify-style server routes
can opt out — the entity file then has no `runtime-ts/drizzle-fastify` imports
at all, and `@metaobjectsdev/runtime-ts` can be dropped from the consumer's
dependency tree entirely:

```ts
// metaobjects.config.ts
import { defineConfig } from "@metaobjectsdev/cli";
import { entityFile } from "./codegen/generators/entity";
import { queriesFile } from "./codegen/generators/queries";
import { barrel } from "./codegen/generators/barrel";

export default defineConfig({
  generators: [entityFile({ allowlists: false }), queriesFile(), barrel()],
});
```

The `entityFile` / `queriesFile` / `routesFile` / `barrel` factories are imported
from the **owned local copies** that `meta init` scaffolds into
`codegen/generators/` (ADR-0034 scaffold-and-own). Importing them from
`@metaobjectsdev/codegen-ts/generators` still works but is **deprecated** — own a
copy instead; the package export will be removed in a future major. The engine and
primitives (`runGen`, the scope helpers `perEntity` / `perPackage` / `perModel`
(`oncePerRun` is a soft-deprecated alias of `perModel`), `RenderContext`, the loader and
render helpers) remain the stable, versioned import from `@metaobjectsdev/codegen-ts`,
and an owned generator imports them from there.

The client-side `<Entity>Filter` type is still emitted regardless — it has zero
runtime-ts dependency and consumers want it for typed client calls. Default is
`true` for back-compat with existing projects.

If you keep `routesFile()` wired in, leave `allowlists` at its default — the
generated routes reference the allowlists by name and won't compile without
them.

### Consumer wiring

Generated query helpers accept a Drizzle `db` instance as their first
parameter. See [wiring-generated-queries.md](../../../../docs/recipes/wiring-generated-queries.md)
for per-dialect setup, edge (Workers / D1) examples, and a 0.6.0 → 0.7.0
migration guide. The cross-language design decision is in
[ADR-0008](../../../../spec/decisions/ADR-0008-parameter-passing-generated-repo-helpers.md).

## Output targets

Each generator can be routed to its own output directory via a named **target**
(see the CLI's `defineConfig` — `@metaobjectsdev/cli` README, "Multiple output
targets"). The runner gives every generator a `RenderContext` carrying its own
`selfTarget` and the shared `entityModuleTarget` (where `entityFile()` output
lives), then writes each emitted file under its target's `outDir` (collisions are
keyed on the resolved full path, so the same filename in two targets is fine).

Templates resolve the entity-module import through `entityModuleSpecifier(selfTarget,
entityModuleTarget, pkg, name, extStyle)`:

- **same target** → relative (`./Program`), honoring `extStyle`
- **cross target** → extension-less package path from the entity-module target's
  `importBase` (`@acme/database/generated/acme/commerce/Program`)

Companion helpers: `siblingSpecifier` (same-target sibling module, e.g.
`<Entity>.columns`) and `barrelModuleSpecifier` (barrel re-exports). A generator
declares it produces the entity module with `emitsEntityModule: true` (set by
`entityFile()`); the runner derives the entity-module target from it. With a single
target, every specifier takes the relative branch, so output is unchanged.

## Dialects

- `sqlite` — emits `sqliteTable`, `text`, `integer`, etc. from `drizzle-orm/sqlite-core`
- `postgres` — emits `pgTable`, `varchar`, `bigint`, etc. from `drizzle-orm/pg-core`

## Driver compatibility

The `type Db = ...` alias at the top of each generated `<Entity>.queries.ts` is
the **base** Drizzle class every driver of that dialect extends, so any
compatible Drizzle instance type-checks:

- **Postgres** → `PgDatabase<PgQueryResultHKT, Record<string, unknown>>` — accepts
  `node-postgres` (`pg`), `postgres.js`, `@neondatabase/serverless`,
  `@vercel/postgres`, and `pglite`.
- **SQLite** → `BaseSQLiteDatabase<"sync" | "async", unknown, Record<string, unknown>>`
  — accepts both the sync driver (`better-sqlite3`) and the async ones (`libsql` /
  Turso / D1).

The trailing `Record<string, unknown>` is the **schema** parameter, held open at
Drizzle's own `TFullSchema extends Record<string, unknown>` bound rather than its
`Record<string, never>` default. That is what lets you pass the idiomatic
schema-carrying database:

```ts
import * as schema from "./generated";
export const db = drizzle(client, { schema });   // assigns to `Db`
export const bare = drizzle(client);             // so does this
```

Reads (`find*ById`, `list*`, and a projection's read-only queries) work on every
one of those drivers.

**Write caveat (`create*` / `update*`).** Those functions use Drizzle's
`.returning()` API, which needs native `RETURNING` support. Every Postgres driver
has it, as do `libsql` / Turso / D1. It does **not** work on `better-sqlite3` or
`bun:sqlite` (no native `RETURNING`) — code still compiles against the `Db` type,
but `create*` / `update*` fail at runtime. On those two drivers, write a
non-`.returning()` replacement in your own sibling module (`<Entity>.extra.ts`) and
import **that** at the call sites — nothing overrides the generated function for you —
or switch to an async SQLite driver.

## `outputParser()` — typed parsers for a responding `template.prompt`

For every `template.prompt` that declares `@responseRef` (ADR-0052 — the inbound tier keys off
`@responseRef`; a `template.output` is outbound-only and gets no parser), `outputParser()` emits
`<PromptName>.response.ts` containing a Zod schema, a dual-API parser, and a tolerant `extract`:

```ts
// metaobjects.config.ts
import { defineConfig } from "@metaobjectsdev/cli";
// Owned generators scaffolded by `meta init` (ADR-0034 scaffold-and-own).
import { entityFile } from "./codegen/generators/entity";
import { queriesFile } from "./codegen/generators/queries";
import { barrel } from "./codegen/generators/barrel";
import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators";

export default defineConfig({
  // entityFile() is required: it emits the value-object interfaces the parser returns.
  generators: [entityFile(), queriesFile(), barrel(), promptRender(), outputParser()],
});
```

For a `template.prompt` named `SupportAnswerPrompt` with `@responseRef: "SupportAnswer"`:

```ts
// Generated SupportAnswerPrompt.response.ts
import { z } from "zod";
import type { SupportAnswer } from "./SupportAnswer.js";   // emitted by entityFile()

const SupportAnswerPromptSchema = z.object({
  text: z.string(),
  confidence: z.enum(["HIGH", "OK", "LOW"]),
  note: z.string().optional(),
});

export type SupportAnswerPromptValidationError = z.ZodError;

/** Throws ZodError on validation failure. */
export function parseSupportAnswerPrompt(text: string): SupportAnswer { ... }

/** Result-style; never throws. */
export function safeParseSupportAnswerPrompt(text: string):
  | { success: true; data: SupportAnswer }
  | { success: false; error: SupportAnswerPromptValidationError } { ... }
```

The parsers return the `@responseRef` value object's own interface — the one `entityFile()`
writes to `SupportAnswer.ts` — not a type of their own (ADR-0056). The tolerant path returns
`SupportAnswerExtracted`, the value object's all-nullable mirror, which the file declares
beside the parser and names after the value object.

Consumer usage:

```ts
import { parseSupportAnswerPrompt, safeParseSupportAnswerPrompt } from "./generated/SupportAnswerPrompt.response";

const answer = parseSupportAnswerPrompt(llmResponseText);   // throws on bad shape

const r = safeParseSupportAnswerPrompt(llmResponseText);
if (!r.success) { /* handle r.error (a ZodError) */ } else { /* use r.data */ }
```

`parse*` / `safeParse*` expect `llmResponseText` to BE the JSON document (a structured-output
or JSON-mode reply). A raw chat reply — prose around a fenced JSON block — fails them with
`invalid JSON: …`. Use the tolerant tier for that; it takes a loaded `MetaRoot` because it
reads the live metadata (there is no text-only variant):

```ts
import { MetaDataLoader } from "@metaobjectsdev/metadata";
import { orThrow } from "@metaobjectsdev/render";
import { extractLenientSupportAnswerPromptWithLoader } from "./generated/SupportAnswerPrompt.response";

const { root } = await MetaDataLoader.fromDirectory("./metaobjects");   // once, at startup
const result = extractLenientSupportAnswerPromptWithLoader(root, rawReply);
result.data;     // SupportAnswerExtracted — what was recovered, every field nullable
result.report;   // per field: recovered / defaulted / lost / malformed
orThrow(result); // opt-in: ExtractError when a @required field was lost
```

A bad reply never throws; `extractLenient…WithLoader` throws only when `root` does not declare
the response value object.

**Field-type → Zod-type mapping:**

| Field subtype | Emitted Zod |
|---|---|
| `field.string` | `z.string()` |
| `field.int`, `field.long` | `z.number().int()` |
| `field.double`, `field.float` | `z.number()` |
| `field.boolean` | `z.boolean()` |
| `field.enum` | `z.enum([...])` |
| `field.object` (with `@objectRef`) | nested `z.object({ ... })` |
| `isArray: true` on any of the above | wrapped in `z.array(...)` |

**Options:**

```ts
outputParser({
  outDir: "src/generated/outputs",   // default: emits at the target's root
  target: "default",                 // default: "default" (the entity-module target)
})
```

**`meta verify` integration:** when `meta verify` runs, every `@payloadRef` and `@responseRef`
resolution is checked. Unresolved refs fail the build (`exit 1`). See
[ADR-0010](../../../../spec/decisions/ADR-0010-template-output-parser-codegen.md) and
[ADR-0052](../../../../spec/decisions/ADR-0052-template-direction-outbound-vs-inbound.md)
for the cross-language design rationale.

## Naming conventions: camelCase TS ↔ snake_case SQL

`@metaobjectsdev/codegen-ts` maps `snake_case` metadata field names to
`camelCase` TS property names by default. The underlying SQL column stays
`snake_case`.

```jsonc
// Metadata
{ "field.long": { "name": "council_id" } }
```

```ts
// Generated TS — property is camelCase
import { councils } from "./generated/Council";
const id = council.councilId;
db.select().from(councils).where(eq(councils.councilId, "abc"));
```

```sql
-- Generated DDL — column stays snake_case
CREATE TABLE councils (
  council_id TEXT NOT NULL PRIMARY KEY,
  ...
);
```

To override the SQL column name per-field, use `@dbColumn`:

```jsonc
{ "field.long": { "name": "councilId", "@dbColumn": "council_uuid" } }
```

The mapping policy is project-wide via `columnNamingStrategy` in
`metaobjects.config.ts`: `snake_case` (default) | `literal` | `kebab-case`.

## License

Apache-2.0.

---
_Source: https://npm.io/package/@metaobjectsdev/codegen-ts · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
