/prisma-import

Bringing import statements to Prisma schemas

Primary LanguageTypeScript

Prisma Import

Bringing import statements to Prisma schemas

VSCode ExtensionCLILanguage Server

❗ This project is no longer maintained

Prisma has released native multi-schema support, addressing exactly what this project aimed to solve. More info:

Thank you!


Description

prisma-import exists in order to address the multiple-schema situation on Prisma. Current solutions only focus on the schema merging part, and not on good DX and integration within the Prisma ecosystem.

This is an attempt to allow importing models and other blocks between schemas, and thus give the developer the flexibility of working with multiple files.

→ See How it works for a more in-depth description


The project is divided into 2 different areas.

VSCode Extension & Language Server

Both the Prisma VSCode extension and the Language Server have been refactored to understand import statements and to provide IDE features that support them (such as syntax highlighting, linting, formatting, etc.)

→ See the VSCode extension README

CLI

The prisma-import CLI can be used to merge all schemas into one that Prisma can understand.

→ See the prisma-import CLI README


Usage

Install the VSCode Prisma Import extension from the marketplace here and disable the official Prisma extension so they don't collide.

Optionally install the prisma-import CLI to merge all your schemas into one.

Import syntax

Import statements work pretty similar to JavaScript's. Basically, they describe a list of blocks (model, enum or type) that are imported from another schema.

They look like this:

// modules/user/user.prisma

import { Post, Comment } from "../blog/post/post"

model User {
  id        Int       @id @default(autoincrement())
  firstName String
  lastName  String
  email     String    @unique
  password  String
  posts     Post[]
  comments  Comment[]
}

// modules/blog/post/post.prisma

import { User } from "../../user/user"

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  description String
  content     String
  user        User      @relation(fields: [userId], references: [id])
  userId      Int
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  comments    Comment[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  user      User     @relation(fields: [userId], references: [id])
  userId    Int
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
  message   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

An import statement is composed of four main parts:

  1. The import keyword
  2. The comma-separated list of blocks to import within brackets ({})
    • Only model, type or enum are allowed
  3. The from keyword
  4. The relative path to the schema containing those blocks
    • It must not include the .prisma extension

Additional notes:

  • Multiple imports from the same path are allowed
  • Duplicate blocks (either in imports or the current schema) are not allowed
  • Import statements must be contained in a single line
  • Import paths are workspace-bound, meaning that only those schemas in your workspace (opened VSCode directory) are importable

How it works

When planning this project, there were two main ways to approach the issue.

The first one was to refactor prisma-engines and prisma-fmt in order to add import statements to the Prisma AST/PSL, but even though I tried, the rabbit hole was too deep. Prisma is built to work with a single schema —not many—, and there's a lot of things to take into account when introducing such a breaking change

  • What happens with introspections when working with imports?
  • How does one solve the implicit circular dependencies of model relations? (related models almost always reference each other)
  • How would Prisma know which files to read? All schemas? Only those specified?
  • Should Prisma merge all schemas into one, or always work with imports?
  • etc, etc

So after giving up on modifying the Prisma core, I focused on how to tweak the Prisma extension and Language Server to understand import statements.

What was clear was that whatever was going to be fed into prisma-fmt should be a single, valid schema (aside from any user errors). With this in mind, in order to avoid not found blocks and other errors that would cause the formatter and linter to misbehave, a virtual schema had to be resolved from the import statements and appended to the current schema. The virtual schema is invisible to the user and is resolved following these steps.

Analyze current schema

Get all the blocks in the current schema as well as identify all import statements together with the imported blocks. This allows us to know what we're looking for and where.

Identify dependencies

Once we have the current schema information, we know which blocks we need to search and on which schema. If there are no imports, then there's nothing to virtualize and no virtual schema should be created.

Recursively resolve blocks

Find imported blocks, identify their relations and recursively resolve them until there are no blocks left.

With a schema file and an array of blocks to search, the steps go roughly like this.

For each block to search

  1. If it is already visited (ie added to the virtual schema), ignore it
  2. If the block is not a direct dependency of the origin schema (the one requesting linting/formatting — the one opened by the user), append VirtualReplaced to its name. (model Usermodel UserVirtualReplaced)

Step 2 is essential. Imagine the origin schema is using the User model and the Post model, but only importing the latter. We would want the linter to catch that and mark the User reference as not defined.

If we were to not rename the User model as UserVirtualReplaced, since Post has User as dependency, both would be added to the virtual schema as is and the linter would assume that User is defined in the origin schema and thus not mark it as error.

  1. Go through all fields of the block, filter out the ones that are not a relation/enum/type (ie. String, Int) and identify the blocks

    • If block is visited, only rename it to <name>VirtualReplaced if necessary (so the linter does not catch it as not found)
    • If block is in current schema, rename if necessary and go to n.1 searching only for this block
    • If block is in imports, rename if necessary, find it in imported schema and go to n.1 searching only for this block
  2. Once all dependencies of the block have been resolved, add it to the virtual schema

Append virtual schema

With everything resolved, the virtual schema can be appended to the end of the current schema. Then, all import statements are commented out so Prisma doesn't complain.

Assuming the following user.schema as the origin schema

// modules/user/user.prisma

import { Post } from "../blog/post/post"

model User {
  id        Int       @id @default(autoincrement())
  firstName String
  lastName  String
  email     String    @unique
  password  String
  posts     Post[]
}

// modules/blog/post/post.prisma

import { User } from "../../user/user"

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  description String
  content     String
  user        User      @relation(fields: [userId], references: [id])
  userId      Int
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  comments    Comment[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  username  String
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
  message   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

This is what Prisma would see, once the virtual schema is resolved

//import { User } from "../../user/user"

model User {
  id        Int       @id @default(autoincrement())
  firstName String
  lastName  String
  email     String    @unique
  password  String
  posts     Post[] // Direct origin dependency -> not renamed
}

// begin_virtual_schema

model Post { // Direct origin dependency -> not renamed
  id          Int                       @id @default(autoincrement())
  title       String
  description String
  content     String
  user        User                      @relation(fields: [userId], references: [id])
  userId      Int
  createdAt   DateTime                  @default(now())
  updatedAt   DateTime                  @updatedAt
  comments    CommentVirtualReplaced[] // Non-direct dependency -> renamed
}

model CommentVirtualReplaced { // Non-direct origin dependency -> renamed
  id        Int      @id @default(autoincrement())
  username  String
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
  message   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

To document

  • How does the extension resolve errors in the virtual schema
  • How does the extension format across schemas

Contributing

All aspects of this project are open to contributions. Feel free to open PRs for bug fixes, improvements and new features.

License

VSCode Extension

Language Server

CLI