# @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.
codegen-ts overwrites files with the header but refuses to touch files
without it. Hand-customizations live in sibling `<Entity>.extra.ts` files
(for custom queries, derived-column indexes, etc. that metadata can't express).

### 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, ...>` — accepts `node-postgres`
  (`pg`), `postgres.js`, `@neondatabase/serverless`, `@vercel/postgres`, and
  `pglite`.
- **SQLite** → `BaseSQLiteDatabase<"sync" | "async", unknown>` — accepts both the
  sync driver (`better-sqlite3`) and the async ones (`libsql` / Turso / D1).

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, override the
affected functions in the sibling `<Entity>.extra.ts` file with a
non-`.returning()` form, or switch to an async SQLite driver.

## `outputParser()` — typed parsers for `template.output`

For every `template.output` declared in your metadata, `outputParser()` emits
`<TemplateName>.output.ts` containing a Zod schema + dual-API parser:

```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";
// Prompt/output generators have no reference template yet — package import.
import { promptRender, outputParser } from "@metaobjectsdev/codegen-ts/generators";

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

For a `template.output` named `NpcResponseOutput` with `@payloadRef: "NpcResponsePayload"`:

```ts
// Generated NpcResponseOutput.output.ts — self-contained, no cross-file imports
import { z } from "zod";

const NpcResponseOutputSchema = z.object({
  name: z.string(),
  age: z.number().int(),
});

export type NpcResponseOutputData = z.infer<typeof NpcResponseOutputSchema>;
export type NpcResponseOutputValidationError = z.ZodError;

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

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

`parseXxx` and `safeParseXxx` return the local `<Name>Data` type, derived from
the schema via `z.infer<>`. It is structurally identical to the payload-VO
interface emitted by `promptRender()` (e.g., `NpcResponsePayload` in
`prompts.ts`) — consumers who wire both generators can assign back and forth
between `NpcResponseOutputData` and `NpcResponsePayload` interchangeably. The
output-parser file is intentionally self-contained so it compiles standalone
even when `promptRender()` is not wired in.

Consumer usage:

```ts
import { parseNpcResponseOutput, safeParseNpcResponseOutput } from "./generated/NpcResponseOutput.output";

const npc = parseNpcResponseOutput(llmResponseText);   // throws on bad shape

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

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

| Field subtype | Emitted Zod |
|---|---|
| `field.string`, `field.class` | `z.string()` |
| `field.int`, `field.long` | `z.number().int()` |
| `field.double`, `field.float` | `z.number()` |
| `field.boolean` | `z.boolean()` |
| `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 `template.output`'s
`@payloadRef` resolution is checked. Unresolved refs fail the build (`exit 1`)
with a `(output)` prefix on the diagnostic. See [ADR-0010](../../../../spec/decisions/ADR-0010-template-output-parser-codegen.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.
