Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ here.**

### Added

- **Prisma as the ORM, declared once in the model (TypeScript, #479).** Set `orm: "prisma"` in
`metaobjects.config.ts` and wire the new `prisma-schema` generator (`prismaSchemaFile()`), and
`meta gen` writes `schema.prisma` from the model for the `postgresql`, `mysql` or `sqlite`
provider. It maps types, ids and their generation, defaults, nullability, enums, relations,
unique keys, indexes and `@map`/`@@map`, and Postgres `@schema` maps to `@@schema`. Prisma cannot
declare CHECK constraints, so the ones the model implies go in `schema.checks.sql`, which you
apply with `prisma db execute --file`. `meta verify --codegen` fails on a hand-edited or stale
schema; the generator overwrites hand edits rather than merging them. On Postgres, a database
created by `prisma db push` with the checks file applied passes `meta verify --db`, and a
change to it fails. On SQLite, which cannot take the checks file, the gate reports each
missing CHECK and nothing else, and a change to the database fails it.
Most of what a Prisma schema cannot express is refused with an `ERR_PRISMA_*` code, never dropped: a
view `meta verify --db` expects, an expression or partial index, a CHECK on SQLite, a `@schema`
outside Postgres, a table with no primary key, a relation Prisma cannot model, a name
collision, or an unsupported provider. The exception is MySQL, which gets the enum CHECKs only:
the other checks are Postgres SQL and are left out. A model with projections, write-through read views or
served reports therefore needs each of those views marked `@unmanaged` or its object left out
by the generator's `filter`. A flattened `field.object` maps to one Prisma field per column.
`orm: "prisma"` also makes every target default to `runtime: false`. It refuses a target set to
`runtime: true`, and the generators that write Drizzle code (`queries`, `routes`,
`routes-hono`, `callable`). The default, `orm: "drizzle"`, changes nothing. The generator is
ejectable (`meta eject prisma-schema`). `@metaobjectsdev/codegen-ts` now depends on
`@metaobjectsdev/migrate-ts` for the expected schema, through the new
`@metaobjectsdev/migrate-ts/expected` subpath, which does not load kysely. Recipe:
`docs/recipes/prisma.md`.

- **Java and Kotlin consume a shared model (FR-023 Phase 2).** A project that declares
`dependencies` in `.metaobjects/config.json` loads its committed `.metaobjects/deps/` snapshot
ahead of its own files, in the Maven plugin (every rung of the source ladder) and the Java
Expand All @@ -25,6 +51,7 @@ here.**
an imported object unless `scope.include` names its package. Runs all 24 resolution cases of
`fixtures/dependency-conformance/` (the 11 check cases pin the Node `meta deps check`); Kotlin loads through the same loader and adds a codegen test.
Spring runtime auto-load and `verify` drift for dependencies are later slices.

- **Java, Kotlin and C# can compile and match `scope` patterns.** `com.metaobjects.config.Scope`
(Java; Kotlin inherits it) and `MetaObjects.Config.Scope` (C#) port the scope-pattern grammar from
TypeScript (`compilePattern` / `compileScope` / `matchesScope`), and both run the shared
Expand Down Expand Up @@ -98,6 +125,20 @@ here.**

### Fixed

- **`@default: "now"` is the current time on Postgres, not the table's creation time.** The
expected schema stored it as the literal `'now'`, and Postgres converts that literal to a
timestamp once, when it creates the column's `DEFAULT`, so every row got the same value. It
is now the expression `now()`, which is what `@autoSet` and the generated Drizzle column
already used. On a database `meta migrate` created from such a field, the next `meta migrate`
proposes a change-column-default, and `meta verify --db` reports the old default until it is
applied.

- **`meta verify --db` on SQLite accepts how other tools spell the same storage.** A column
declared `BOOLEAN` now matches the model's `INTEGER`, `DECIMAL(p,s)` matches `NUMERIC`, and an
unquoted `TRUE`/`FALSE` default matches `1`/`0`. SQLite stores each pair identically. Prisma
writes the first of each, so before this fix a database pushed by Prisma reported drift on
every boolean and decimal column.

- **`meta deps sync` and `meta deps check` finish cleanly on a project whose own metadata the
Node toolchain cannot read.** A Maven project (`src/main/resources/metadata`), a C# project, or a
model using vocabulary only another port registers declares no neutral `sources`, so the closing
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ first-week wedge plan — and `meta init` picks up from there.
| Payload-VO codegen (the value object's own type, ADR-0056) | Yes (`entityFile()`) | Yes (`SpringValueObjectGenerator`) | Yes (`KotlinEntityGenerator`) | Yes (`EntityGenerator`) | Yes (`entity`) |
| Migration emission | `meta migrate` (Postgres / SQLite / D1) | Via TS toolchain (`@metaobjectsdev/cli migrate`) | Via TS toolchain (`@metaobjectsdev/cli migrate`) | Via TS toolchain (ADR-0015) | Via TS toolchain (ADR-0015) |
| DB-drift verify | `meta verify --db <url>` | Template-drift: `Verify.check`; schema-drift is TS-owned (ADR-0015) | Template-drift: `Verify.check`; startup: `MetadataStartupValidator` | `dotnet meta verify` (codegen-drift) | Schema-drift is TS-owned (ADR-0015) |
| Prisma schema (`orm: "prisma"`, #479) | `prismaSchemaFile()` writes `schema.prisma` (Postgres / MySQL / SQLite); `verify --codegen` and `verify --db` gate it | No | No | No | No |
| Template-drift verify | Yes | Yes (`Verify.check`) | Yes (via Java) | Yes (`dotnet meta verify`) | Yes (`metaobjects.render.verify`) |
| YAML authoring (sigil-free → JSON) | Yes | Yes | Yes (via Java) | Yes | Yes |
| Capability requirements (`requirement.*`) | `meta verify` gate + authoring lint; `requirementTests()` stubs | `mvn metaobjects:verify` gate; JUnit Jupiter `requirement-tests` | Gate via the Java Maven goal; JUnit Jupiter `requirement-tests` | `dotnet meta verify` gate; xUnit `requirement-tests` | `metaobjects verify` gate; pytest `requirement-tests` |
Expand Down
1 change: 1 addition & 0 deletions agent-context/skills/metaobjects-codegen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ exact config, generator names and commands for this project; read it. The other
| `references/dependencies-and-shared-models.md` | the project consumes or publishes metadata across projects |
| `references/typescript-document-store.md` | a TypeScript project on MongoDB, or any store MetaObjects does not manage, wanting a tested repository generator |
| `references/typescript-mysql.md` | a TypeScript project on a MySQL database |
| `references/typescript-prisma.md` | a TypeScript project that keeps Prisma as its ORM |
| `references/typescript-retargeting.md` | a TypeScript project on a framework other than Fastify, Hono or React |
| `references/typescript-templates.md` | declarative Mustache template-codegen in a TypeScript project |
| `references/typescript-docs.md` | the docs generators (`docsFile`, `apiDocsFile`, `agentDocsFile`, `requirementsFile`, `metamodelDocsFile`, `docsSiteFile`) in a TypeScript project |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ reference generators target React and TanStack. Anything else is this page.
| Layer | Always available | When your target is not built in |
|---|---|---|
| **model** — types, create/PATCH validation, filter and sort allowlists | yes, in every port, for every store | — |
| **persistence** — how rows are read and written | Postgres, SQLite, D1; MySQL (TypeScript, Java) | §3 |
| **persistence** — how rows are read and written | Postgres, SQLite, D1; MySQL (TypeScript, Java); a Prisma schema (TypeScript) | §3 |
| **api** — the HTTP surface | Fastify, Hono | §4 |
| **client** — hooks, forms, grids | React, TanStack | §5 |

Expand All @@ -52,6 +52,12 @@ the column-type table and the behaviour differences are in `typescript-mysql.md`
OMDB's `MySQLDriver` reads and writes MySQL; the generated DTOs and controllers are
dialect-neutral.

**Prisma as the ORM (TypeScript).** Set `orm: "prisma"` and wire `prismaSchemaFile()`. The
model then writes `schema.prisma` (Postgres, MySQL, SQLite) beside Zod schemas and types, and
Prisma runs the database. `meta verify --codegen` and `--db` gate it. Setup, the mapping and
what it refuses are in `typescript-prisma.md`. Any other ORM (TypeORM, Sequelize, MikroORM) is
a generator you write: the Prisma one is a model to copy (`meta eject prisma-schema`).

**A SQL database that speaks another dialect's protocol.** Use that dialect: CockroachDB,
Neon, Supabase, AlloyDB and Aurora PostgreSQL are `postgres`; Turso and libSQL are `sqlite`
(or `d1` on Cloudflare); MariaDB and PlanetScale are `mysql`. Test the generated queries
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Prisma on TypeScript

> Part of the `metaobjects-codegen` skill's TypeScript reference. Read it when the app keeps
> Prisma as its ORM.

Under `orm: "prisma"`, `meta gen` writes `schema.prisma` from the model, along with Zod schemas
and types that have no Drizzle code. Prisma still creates the tables (`prisma db push` or
`prisma migrate`) and runs the queries. `meta verify --codegen` fails when someone edits
`schema.prisma` or it is out of date. `meta verify --db` fails when the database Prisma created
differs from the model (Postgres and SQLite). Do not hand-write a Prisma model for a table the
MetaObjects model declares.

## Contents
- TypeScript setup
- The workflow
- How the model maps
- What it refuses
- SQLite and MySQL
- Eject it

## TypeScript setup

```ts
// metaobjects.config.ts
import { defineConfig } from "@metaobjectsdev/cli";
import { prismaSchemaFile } from "@metaobjectsdev/codegen-ts/generators";

export default defineConfig({
orm: "prisma",
dialect: "postgres", // "postgres" | "mysql" | "sqlite": picks the Prisma provider
outDir: "src/generated", // Zod schemas and types
targets: { prisma: { outDir: "prisma" } },
generators: ["entity", prismaSchemaFile({ target: "prisma" })],
});
```

`orm: "prisma"` does two things:

- **Every target defaults to `runtime: false`.** The `entity` generator then writes Zod schemas
and types with no `drizzle-orm` import. A target that sets `runtime: true` is refused.
- **Generators that write Drizzle code are refused** by name: `queries`, `routes`,
`routes-hono` and `callable`. Your routes call the Prisma Client instead. `meta gen` exits
with `orm: "prisma" refuses "queries", …` and makes no changes.

The default `orm` is `"drizzle"`, and it changes nothing.

Prisma 7 reads the connection URL from `prisma.config.ts`, not from the schema:

```ts
// prisma.config.ts
import { defineConfig } from "prisma/config";
export default defineConfig({ schema: "prisma/schema.prisma", datasource: { url: process.env.DATABASE_URL! } });
```

The generated schema names the `prisma-client` generator, writing to `prisma/generated`. To
choose your own generator and datasource blocks, pass `header` as a string. Pass
`header: false` for a multi-file schema (`prisma/schema/`) whose blocks live in a file you own.
Other options:

- `provider` overrides the provider that `dialect` picks.
- `fileName` renames the file (default `schema.prisma`).
- `filter` narrows which objects get a model.
- `checks: "omit"` drops the CHECK file.

## The workflow

```bash
meta gen # writes prisma/schema.prisma (+ schema.checks.sql)
npx prisma db push # or: npx prisma migrate dev
npx prisma db execute --file prisma/schema.checks.sql
npx prisma generate # the Prisma Client, as usual
meta verify --codegen --db "$DATABASE_URL" # in CI: fails on an edited schema or a drifted database
```

To change a field, edit the model and run `meta gen`. **Do not edit `schema.prisma` by hand.**
The generator overwrites hand edits instead of merging them. Prisma treats the whole file as
the schema, so a merged edit would change the database without the model knowing.
`meta verify --codegen` fails on the edit until you run `meta gen` again. `prisma format`
leaves the file unchanged, so formatting it does not count as an edit.

A Prisma schema cannot declare a CHECK constraint, so the constraints the model implies go in
`schema.checks.sql`. These are enum membership checks plus the `@min`/`@max` and length
validators. Apply the file after every `db push` or migration. On Postgres the file drops each
constraint before adding it, so you can apply it more than once. On MySQL it only adds them,
so apply it once, when you create the table. When a check changes, drop the old one in a Prisma
migration. When the model has no checks left, `meta gen` deletes the file.

With `prisma migrate`, keep the Prisma migration chain: `meta gen` changes `schema.prisma`, and
`prisma migrate dev` writes the SQL. To make the checks part of the chain, put the contents of
`schema.checks.sql` into a migration (`prisma migrate dev --create-only`, then add the SQL).

## How the model maps

The schema is built from the same expected schema that `meta migrate` writes and
`meta verify --db` compares against. A database that Prisma creates from it therefore passes
the gate.

| Model | Prisma |
|---|---|
| object with a writable `source.rdb` | `model <Name>` with `@@map("<table>")` when the names differ |
| field | a scalar field with `@map("<column>")` when the names differ; `?` when the field is not `@required` |
| `field.string @maxLength` | `String @db.VarChar(n)` (Postgres, MySQL) |
| `field.int` / `field.long` / `field.currency` | `Int` / `BigInt` / `BigInt` |
| `field.double` / `field.float` | `Float` / `Float @db.Real` (Postgres) |
| `field.decimal` | `Decimal @db.Decimal(p, s)` |
| `field.boolean` | `Boolean` |
| `field.timestamp` / `field.date` / `field.time` | `DateTime` with `@db.Timestamptz`/`@db.Timestamp`, `@db.Date`, `@db.Time` (Postgres); `String` on SQLite, where the column holds ISO text |
| `field.uuid` | `String @db.Uuid` (Postgres), `@db.VarChar(36)` (MySQL) |
| `field.enum` | `String` plus a CHECK in `schema.checks.sql` (not a Prisma `enum`, so the column matches `meta migrate`'s) |
| `field.object`, `field.map`, `@storage: jsonb` | `Json` |
| `field.object @storage: flattened` | one field per field of the value object, `<field><Nested>` with `@map("<column>_<nested_column>")` |
| `isArray` scalar | a list (`String[]`) on Postgres; `Json` on MySQL; `String` holding JSON text on SQLite |
| `identity.primary` | `@id` or `@@id([...])`; `@generation: increment` is `@default(autoincrement())`; `@generation: uuid` is `gen_random_uuid()` on Postgres, `uuid()` elsewhere |
| `@default` | `@default(...)` for a literal; `"now"` and other expressions are `dbgenerated(...)`, so the default text matches what `verify --db` expects (a MySQL `DateTime` takes `now()`) |
| `identity.secondary` | `@unique` or `@@unique` (the index name in `map:`) |
| `index.lookup` | `@@index` (`@orders` becomes `sort: Desc`) |
| `identity.reference` | a relation field on both models, with `fields`, `references`, `onDelete`, `onUpdate` and the constraint name in `map:` |
| `@schema` on `source.rdb` | `@@schema("<schema>")` and the datasource's `schemas` list (Postgres) |

## What it refuses

The generator stops with an error code rather than write a schema that would push a database
the model does not describe. Each message names the object or index, says why, and says what
to change.

| Code | When | What to do |
|---|---|---|
| `ERR_PRISMA_READ_ONLY_SOURCE` | an object declares a view that `meta migrate` creates and `meta verify --db` expects (a projection, a write-through entity's read view, or a served report) | create the view in a Prisma migration and mark its read `source.rdb` `@unmanaged: true`, or leave the object out with `filter` |
| `ERR_PRISMA_UNSUPPORTED_INDEX` | an index has `@expr` or `@where`, or a `@using` method other than `btree` or, on Postgres, `hash`, `gin`, `gist`, `brin` or `spgist` | create it in a Prisma migration, or drop the attribute |
| `ERR_PRISMA_UNSUPPORTED_SCHEMA` | a `source.rdb` declares `@schema` and the provider is not `postgresql`, where Prisma has no `@@schema` | remove the `@schema`, or use the `postgresql` provider |
| `ERR_PRISMA_UNSUPPORTED_CHECK` | the provider is SQLite and the model implies a CHECK | pass `checks: "omit"` |
| `ERR_PRISMA_NO_PRIMARY_KEY` | a table has no primary key | declare an `identity.primary` |
| `ERR_PRISMA_UNMAPPABLE_RELATION` | a foreign key points at a table with no model, at a non-unique key, or at a column of another type | include the table, reference a key, or match the subtype |
| `ERR_PRISMA_NAME_COLLISION` | two objects, or two fields, would get the same Prisma name | rename one, or narrow `filter` |
| `ERR_PRISMA_UNSUPPORTED_PROVIDER` | the dialect or `provider` is not `postgresql`, `mysql` or `sqlite` | pass `provider` |

A model with projections, write-through read views or served reports therefore needs one of
those two fixes before the generator runs. A stored procedure, table function or materialized
view is not refused: `meta migrate` does not create it, so neither does Prisma. Read the view's rows with Prisma's `$queryRaw`, and validate them with the
generated Zod schema.

## SQLite and MySQL

- **SQLite** cannot add a CHECK to an existing table. Pass `checks: "omit"`. `meta verify --db`
then reports each missing CHECK as an `add-check` row and nothing else. Only the Zod schemas
enforce those rules. Timestamps, dates and times are `String` in the Prisma Client, because the
column holds ISO text, as it does under `meta migrate`.
- **MySQL** gets the enum CHECKs only. The other checks are written in Postgres SQL. The schema
matches the column types in the MySQL recipe, but `meta verify --db` does not support MySQL,
so on MySQL the drift gate is `verify --codegen` alone.

## Eject it

```bash
meta eject prisma-schema
```

This copies the generator to `codegen/generators/prisma-schema.ts`, and the mapping and
renderers to `codegen/templates/prisma/`. To change a type mapping, edit your copy. A mapping
that no longer matches the expected schema makes `meta verify --db` report drift.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ packages. Codegen runs through the Node `meta` CLI (`@metaobjectsdev/cli`, binar
- Field subtype → column mapping

Topic files beside this one, to open when they come up:
`typescript-mysql.md` (a MySQL database), `typescript-document-store.md` (MongoDB or any
store MetaObjects does not manage, with a tested repository generator),
`typescript-mysql.md` (a MySQL database), `typescript-prisma.md` (Prisma as the ORM),
`typescript-document-store.md` (MongoDB or any store MetaObjects does not manage, with a
tested repository generator),
`typescript-retargeting.md` (a framework other than Fastify, Hono or React),
`typescript-templates.md` (declarative Mustache template-codegen) and `typescript-docs.md`
(the docs generators). The procedure for any unsupported stack is `any-stack.md`.
Expand Down Expand Up @@ -85,6 +86,7 @@ import { tanstackQuery, tanstackGrid } from "@metaobjectsdev/codegen-ts-tanstack
export default defineConfig({
outDir: "src/generated",
dialect: "postgres", // "postgres" | "sqlite" | "d1" | "mysql" (mysql: codegen + runtime; migrate does not own it)
// orm: "prisma", // keep Prisma: contract-only targets + prismaSchemaFile(); see `typescript-prisma.md`
extStyle: "js", // "js" (default) for Node ESM / plain tsc; "none" for a bundler-resolution toolchain — see `owning-generators.md` "Your framework isn't the default"
apiPrefix: "/api", // flows to routes AND client fetch URLs
timestampMode: "string", // "string" (default, ISO-8601 wire contract) | "date" (Drizzle native Date)
Expand Down
Loading
Loading