npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@metaobjectsdev/codegen-ts

v1.0.13

Published

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

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

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

Usage

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.

// 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.)

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:

// 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 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.

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:

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:

// 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":

// 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:

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):

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:

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 and ADR-0052 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.

// Metadata
{ "field.long": { "name": "council_id" } }
// Generated TS — property is camelCase
import { councils } from "./generated/Council";
const id = council.councilId;
db.select().from(councils).where(eq(councils.councilId, "abc"));
-- 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:

{ "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.