Importing a Prisma schema
prisma:import converts a schema.prisma into Prorm model files plus one
initial migration. It is a pure translation — it reads a schema file and
writes source; it never connects to a database, and it never runs a migration.$ npx prorm prisma:import --schema ./prisma/schema.prisma --models-out ./src/models --migrations-out ./migrations
Created model: src/models/User.ts
Created model: src/models/Post.ts
Created model registry: src/models/index.ts
Created migration: migrations/1790460002759-prisma-import.js
Converted 2 model(s) from ./prisma/schema.prisma. Review the generated files — see the comment at the top of the migration file for what was translated on a best-effort basis.
orm-cli is the same binary under its older name.| Flag | Default |
|---|
--schema <path> | ./schema.prisma |
--models-out <path> | ./models |
--migrations-out <path> | ./migrations |
one <Model>.ts per Prisma model block, using the decorator API,
a models/index.ts barrel exporting every model plus a
registerModels(prorm) helper,
<timestamp>-prisma-import.js — a single "initial schema" migration covering
every table and relation.
A worked import#
The Prisma schema#
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
USER
ADMIN
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
body String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
Run the import#
$ npx prorm prisma:import --schema ./prisma/schema.prisma --models-out ./src/models --migrations-out ./migrations
prisma/
schema.prisma # unchanged: the import only reads it
src/
models/
User.ts # @Table class, @HasMany(() => Post)
Post.ts # @BelongsTo(() => User), authorId: ForeignKey<number>
index.ts # export { User, Post } and registerModels()
migrations/
1790460002759-prisma-import.js # createTable x2, addConstraint x1
Read what came out#
Post owns the foreign key, so it gets the @BelongsTo and User gets the
inverse @HasMany. @default(now()) became @Default(DataTypes.NOW) on the
model and CURRENT_TIMESTAMP in the migration; the enum became a string-literal
type plus DataTypes.ENUM:type Role = 'USER' | 'ADMIN';
@Table({ tableName: 'User', timestamps: false })
export class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
@Attribute(DataTypes.INTEGER())
@PrimaryKey()
@AutoIncrement()
declare id: CreationOptional<number>;
@Attribute(DataTypes.STRING(191))
@Unique()
@NotNull()
declare email: string;
@Attribute(DataTypes.STRING(191))
declare name: string | null;
@Attribute(DataTypes.ENUM('USER', 'ADMIN'))
@Default('USER')
@NotNull()
declare role: CreationOptional<Role>;
@Attribute(DataTypes.DATE())
@Default(DataTypes.NOW)
@NotNull()
declare createdAt: CreationOptional<Date>;
@HasMany(() => Post, { foreignKey: 'authorId', as: 'posts' })
declare posts: HasMany<Post>;
}
@Table({ tableName: 'Post', timestamps: false })
export class Post extends Model<InferAttributes<Post>, InferCreationAttributes<Post>> {
@Attribute(DataTypes.INTEGER())
@PrimaryKey()
@AutoIncrement()
declare id: CreationOptional<number>;
@Attribute(DataTypes.STRING(191))
@NotNull()
declare title: string;
@Attribute(DataTypes.STRING(191))
declare body: string | null;
@Attribute(DataTypes.BOOLEAN())
@Default(false)
@NotNull()
declare published: CreationOptional<boolean>;
@Attribute(DataTypes.INTEGER())
@NotNull()
declare authorId: ForeignKey<number>;
@BelongsTo(() => User, { foreignKey: 'authorId', as: 'author' })
declare author: BelongsTo<User>;
}
The migration file also starts with a comment block and
const { DataTypes } = require('prorm');, omitted above.Fix up, then migrate#
[!WARNING] Check the generated imports and foreign-key actions
The generated files import from 'prorm' (import { Model, DataTypes, … } from 'prorm',
require('prorm')). If your project depends on ts-prorm-orm, replace that
specifier before compiling. Every foreign key in the migration is created
ON UPDATE CASCADE ON DELETE CASCADE whatever onDelete: the Prisma
@relation said, so deleting a User deletes their posts unless you change
it.
As a library#
The conversion is exported, so you can drive it yourself — in a codemod, a
test, or your own tooling:import { convertPrismaSchema, parsePrismaSchema, resolveRelations } from 'ts-prorm-orm';
const { models, modelsIndex, migration } = convertPrismaSchema(schemaSource);
// models: { name, source }[] modelsIndex: string migration: string
A codemod that writes the output somewhere the CLI does not, for example
straight into a package in a monorepo, and patches the import specifier on the
way:import fs from 'node:fs';
import path from 'node:path';
import { convertPrismaSchema, parsePrismaSchema } from 'ts-prorm-orm';
const source = fs.readFileSync('prisma/schema.prisma', 'utf8');
// Inspect first: which models and enums are there?
const parsed = parsePrismaSchema(source);
console.log(`${parsed.models.length} models, ${parsed.enums.length} enums`);
// Throws, naming the model and field, on anything it will not guess at.
const { models, modelsIndex, migration } = convertPrismaSchema(source);
const outDir = 'packages/db/src/models';
fs.mkdirSync(outDir, { recursive: true });
const fix = (code: string) => code.replaceAll("'prorm'", "'ts-prorm-orm'");
for (const m of models) fs.writeFileSync(path.join(outDir, `${m.name}.ts`), fix(m.source));
fs.writeFileSync(path.join(outDir, 'index.ts'), fix(modelsIndex));
fs.writeFileSync(`packages/db/migrations/${Date.now()}-prisma-import.js`, fix(migration));
parsePrismaSchema(source) gives the parsed PrismaSchema
(models, enums, fields) if you only want to inspect it, and
resolveRelations(models, names) resolves the relation graph on its own.Nothing in this module touches the filesystem — the CLI owns reading and
writing — which is why it is straightforward to unit test.Type mapping#
| Prisma | Prorm |
|---|
String | DataTypes.STRING(191) |
Int | DataTypes.INTEGER() |
BigInt | DataTypes.BIGINT() |
Float | DataTypes.FLOAT() |
Decimal | DataTypes.DECIMAL(65, 30) |
Boolean | DataTypes.BOOLEAN() |
DateTime | DataTypes.DATE() |
Json | DataTypes.JSON() |
Bytes | DataTypes.BLOB() |
an enum block | DataTypes.ENUM('a', 'b', …) |
String maps to 191 characters because that is the longest utf8mb4 column
MySQL can index under the older 767-byte key limit. Widen it after import if
your target does not have that constraint.What it refuses to convert#
The importer throws rather than guessing. Each of these stops the run with a
message naming the model and field, and telling you what to change:Implicit many-to-many relations — both sides declared as plain arrays with
no @relation. Add an explicit join model, which Prorm needs anyway for
belongsToMany. A relation with no owning side — neither side declares
@relation(fields: [...], references: [...]), so nothing says which table
holds the foreign key.
Composite foreign keys — a relation referencing more than one column.
An unmapped scalar type — anything outside the table above. The error
lists the scalars it does support.
That is deliberate: a silently mis-converted relation surfaces as data
corruption weeks later, while a failed import surfaces now, with a fix in the
message.The commonest refusal, and its fix. An implicit many-to-many:model Post {
id Int @id @default(autoincrement())
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
posts Post[]
}
stops the run with exit code 1 and writes nothing:$ npx prorm prisma:import --schema ./prisma/schema.prisma
Error converting schema: prisma-migrate: relation between "Post" and "Tag" has no side declaring @relation(fields: [...], references: [...]) — can't determine which side owns the foreign key. This looks like an implicit many-to-many relation (both sides are plain arrays with no @relation attribute) — those aren't supported; add an explicit join model instead.
Make the join table a model with the two owning relations, and the import
goes through, producing Post, Tag and PostTag:Then declare the belongsToMany through PostTag on both models by hand, as
the importer emits only the two belongsTo / hasMany pairs.After the import#
The generated output is a starting point. Before you run the migration:1.
Read the migration. It is the whole schema in one file; make sure it
matches the database you actually have (or intend to have).
2.
Check the types. String(191), Decimal(65,30) and DATE are
reasonable defaults, not necessarily your column definitions.
3.
Add what Prisma does not express — hooks, scopes, validations,
paranoid, and any index Prisma held in a @@index that you want tuned.
4.
Diff against the live database with
SchemaDiffer if the schema already exists, rather
than running the initial migration over it. Register the models the barrel file gives you, and apply the migration
rather than sync() so the schema has a history from day one:import { Prorm } from 'ts-prorm-orm';
import { registerModels, User, Post } from './models';
const prorm = new Prorm({ dialect: 'postgres', url: process.env.DATABASE_URL });
registerModels(prorm); // calls prorm.addModels([User, Post])
await prorm.connect();
const ada = await User.create({ email: 'ada@example.com', name: 'Ada' });
await Post.create({ title: 'Notes', authorId: ada.id });
[!CAUTION] Moving a live Prisma database
When the tables already exist because Prisma created them, do not run the
initial migration over them: its createTable calls fail on the first
existing table, or worse, succeed against a different schema. Diff the models
against the database with SchemaDiffer first, then
record the migration as applied by inserting its file name (without .js)
into the tracking table, so migration:run skips it:
Modified at 2026-10-04 00:40:16