gieter 0.4.0

Command-line code generator that turns database schemas into typed source code
gieter-0.4.0 is not a library.

gieter (Dutch for watering can) introspects a live database and emits typed source code from it. It reads your schema, tables, columns, enums, keys, etc. and turns it into a neutral intermediate representation, then hands that to pluggable emitters to generate code in the language of your choice.

Today it ships a PostgreSQL source, a TypeScript emitter, and a Zod emitter.

Features

  • Live introspection of PostgreSQL schemas (tables, columns, enums, primary & foreign keys, comments).
  • Typed TypeScript output with configurable type/interface, enum, and null styles.
  • Runtime validation with Zod schemas, each paired with its static type.
  • Branded ID types; nominal types for primary/foreign keys so a UserId can't be passed where a PostId belongs.
  • Scalar type overrides with automatic import hoisting.
  • Flexible file layout; split kinds across files or emit everything into one.
  • Extensible by design; sources and emitters are just crates implementing a gieter_core trait.
  • Language-agnostic plugins; an external process can act as a source or emitter by exchanging the schema IR as JSON over stdin/stdout, so a plugin can be written in any language.

Implemented emitters

The following implementers are currently implemented:

Missing features

Things that are currently not supported by gieter but are planned are:

  • Python emitter.
  • Rust emitter.

Install

cargo install gieter

Or build from source:

git clone https://github.com/stevenliebregt/gieter
cd gieter
cargo install --path crates/gieter

Quick start

Create a gieter.toml:

[source]
type = "postgres"
url = "postgres://user:password@localhost:5432/db"
schemas = ["public"]

[[emitter]]
type = "typescript"
out_dir = "src/db"
output = "schema.ts"

Then run:

gieter # uses ./gieter.toml
gieter --config path/to/gieter.toml

The database URL may be read from the environment with url = "env:DATABASE_URL". See examples/ for a fully-annotated config.

Extending gieter

There are two ways to add a source or emitter: in-process in Rust, or an external process in any language.

In-process (Rust)

Everything hangs off gieter_core, which defines the schema IR plus the Source and Emitter traits. Depend on it, implement the trait, and register a factory in your own binary alongside the built-ins:

[dependencies]
gieter_core = "<version>"
let mut sources = SourceRegistry::default();
sources.register("my-db", my_crate::factory);

External process (any language)

An external process can be a source or emitter without any Rust. gieter runs the command you configure and exchanges the schema IR as JSON over the process's stdin and stdout, so the plugin can be written in Python, Node, Go, or anything that reads stdin and writes stdout. The command is an argv array, so it works the same across platforms.

[source]
type = "external"
command = ["python3", "introspect.py"]
schemas = ["public"]

[[emitter]]
type = "external"
command = ["node", "emit.js"]
out_dir = "generated"
  • A source receives a SourceRequest ({ ir_version, schemas, options }) on stdin and writes a SourceResponse ({ ir_version, catalog }) to stdout.
  • An emitter receives an EmitRequest ({ ir_version, catalog, options }) and writes an EmitResponse ({ ir_version, files, warnings }).

Every key in the config block except the transport keys command and timeout is forwarded to the plugin as its options. A plugin that runs longer than timeout seconds (default 120) is stopped with an error. Print the exact JSON Schema for any message so you can generate types for your plugin:

See the examples/external/markdown-emitter example which has both an external source and an external emitter.

gieter schema source-request
gieter schema emit-response

The ir_version field guards the contract: if a plugin speaks a version gieter does not expect, gieter stops with a clear message instead of misreading the data.

License

Licensed under either of MIT or Apache-2.0 at your option.