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
UserIdcan't be passed where aPostIdbelongs. - 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_coretrait. - 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:
- Plain TypeScript (
typescript), see the example configs. - Zod TypeScript (
typescript_zod), see the example configs.
Missing features
Things that are currently not supported by gieter but are planned are:
- Python emitter.
- Rust emitter.
Install
Or build from source:
Quick start
Create a gieter.toml:
[]
= "postgres"
= "postgres://user:password@localhost:5432/db"
= ["public"]
[[]]
= "typescript"
= "src/db"
= "schema.ts"
Then run:
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:
[]
= "<version>"
let mut sources = default;
sources.register;
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.
[]
= "external"
= ["python3", "introspect.py"]
= ["public"]
[[]]
= "external"
= ["node", "emit.js"]
= "generated"
- A source receives a
SourceRequest({ ir_version, schemas, options }) on stdin and writes aSourceResponse({ ir_version, catalog }) to stdout. - An emitter receives an
EmitRequest({ ir_version, catalog, options }) and writes anEmitResponse({ 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.
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.