validate.test.ts•16.5 kB
import { stripVTControlCharacters } from 'node:util'
import { serialize } from '@prisma/get-platform/src/test-utils/jestSnapshotSerializer'
import path from 'path'
import { isRustPanic, validate } from '../..'
import { getSchemaWithPath } from '../../cli/getSchema'
import type { MultipleSchemas, SchemaFileInput } from '../../utils/schemaFileInput'
import { fixturesPath } from '../__utils__/fixtures'
jest.setTimeout(10_000)
function restoreEnvSnapshot(snapshot: NodeJS.ProcessEnv) {
  for (const key of Object.keys(process.env)) {
    if (!(key in snapshot)) {
      delete process.env[key]
    }
  }
  for (const [key, value] of Object.entries(snapshot)) {
    if (value === undefined) {
      delete process.env[key]
    } else {
      process.env[key] = value
    }
  }
}
if (process.env.CI) {
  // 10s is not always enough for the "big schema" test on macOS CI.
  jest.setTimeout(60_000)
}
describe('validate', () => {
  // Note: to run these tests locally, prepend the env vars `FORCE_COLOR=0` and `CI=1` to your test command,
  // as `chalk` follows different conventions than the Rust `colored` crate (and uses `FORCE_COLOR=0` to disable colors rather than `NO_COLOR=1`).
  describe.skip('colors', () => {
    // backup env vars
    const OLD_ENV = { ...process.env }
    beforeEach(() => {
      // jest.resetModules()
      restoreEnvSnapshot(OLD_ENV)
      delete process.env.NO_COLOR
      process.env.FORCE_COLOR = '0'
      process.env.CI = '1'
    })
    afterEach(() => {
      // reset env vars to backup state
      restoreEnvSnapshot(OLD_ENV)
    })
    test('failures should have colors by default', () => {
      expect.assertions(1)
      const schema = `
        datasource db {
      `
      const schemas: MultipleSchemas = [['/* schemaPath */', schema]]
      try {
        validate({ schemas })
      } catch (e) {
        expect(e.message).toMatchInlineSnapshot(`
          "Prisma schema validation - (validate wasm)
          Error code: P1012
          [1;91merror[0m: [1mError validating: This line is invalid. It does not start with any known Prisma schema keyword.[0m
            [1;94m-->[0m  [4mschema.prisma:2[0m
          [1;94m   | [0m
          [1;94m 1 | [0m
          [1;94m 2 | [0m        [1;91mdatasource db {[0m
          [1;94m 3 | [0m      
          [1;94m   | [0m
          Validation Error Count: 1
          [Context: validate]
          Prisma CLI Version : 0.0.0"
        `)
      }
    })
    // Note(jkomyno): this fails locally because the colored crate used in Wasm forces the coloring on tty (but apparently not on CI?).
    // On standard terminals, the NO_COLOR env var is actually working as expected (it prints plain uncolored text).
    // See: https://github.com/prisma/prisma-private/issues/210
    test('failures should not have colors when the NO_COLOR env var is set', () => {
      process.env.NO_COLOR = '1'
      expect.assertions(1)
      const schema = `
        datasource db {
      `
      const schemas: MultipleSchemas = [['/* schemaPath */', schema]]
      try {
        validate({ schemas })
      } catch (e) {
        expect(e.message).toMatchInlineSnapshot(`
          "Prisma schema validation - (validate wasm)
          Error code: P1012
          error: Error validating: This line is invalid. It does not start with any known Prisma schema keyword.
            -->  schema.prisma:2
             | 
           1 | 
           2 |         datasource db {
           3 |       
             | 
          Validation Error Count: 1
          [Context: validate]
          Prisma CLI Version : 0.0.0"
        `)
      }
    })
  })
  describe('errors', () => {
    describe('single file', () => {
      test('model with autoincrement should fail if sqlite', () => {
        expect.assertions(1)
        const schema = `
          datasource db {
            provider = "sqlite"
            url      = "file:dev.db"
          }
          model User {
            id        Int      @default(autoincrement())
            email     String   @unique
            @@map("users")
          }`
        const schemas: MultipleSchemas = [['schema.prisma', schema]]
        try {
          validate({ schemas })
        } catch (e) {
          expect(stripVTControlCharacters(e.message)).toMatchInlineSnapshot(`
            "Prisma schema validation - (validate wasm)
            Error code: P1012
            error: Error parsing attribute "@default": The \`autoincrement()\` default value is used on a non-id field even though the datasource does not support this.
              -->  schema.prisma:7
               | 
             6 |           model User {
             7 |             id        Int      @default(autoincrement())
             8 |             email     String   @unique
               | 
            error: Error parsing attribute "@default": The \`autoincrement()\` default value is used on a non-indexed field even though the datasource does not support this.
              -->  schema.prisma:7
               | 
             6 |           model User {
             7 |             id        Int      @default(autoincrement())
             8 |             email     String   @unique
               | 
            Validation Error Count: 2
            [Context: validate]
            Prisma CLI Version : 0.0.0"
          `)
        }
      })
      test('model with autoincrement should fail if mysql', () => {
        expect.assertions(1)
        const schema = `
          datasource db {
            provider = "mysql"
            url      = env("MY_MYSQL_DB")
          }
          model User {
            id        Int      @default(autoincrement())
            email     String   @unique
            @@map("users")
          }`
        const schemas: MultipleSchemas = [['schema.prisma', schema]]
        try {
          validate({ schemas })
        } catch (e) {
          expect(stripVTControlCharacters(e.message)).toMatchInlineSnapshot(`
            "Prisma schema validation - (validate wasm)
            Error code: P1012
            error: Error parsing attribute "@default": The \`autoincrement()\` default value is used on a non-indexed field even though the datasource does not support this.
              -->  schema.prisma:7
               | 
             6 |           model User {
             7 |             id        Int      @default(autoincrement())
             8 |             email     String   @unique
               | 
            Validation Error Count: 1
            [Context: validate]
            Prisma CLI Version : 0.0.0"
          `)
        }
      })
      test(`throws an error when the given datamodel is of the wrong type`, () => {
        expect.assertions(2)
        try {
          // @ts-expect-error
          validate({ schemas: [[true, true]] })
        } catch (e) {
          expect(isRustPanic(e)).toBe(true)
          expect(serialize(e.message)).toMatchInlineSnapshot(`
            ""RuntimeError: panicked at prisma-fmt/src/validate.rs:0:0:
            Failed to deserialize ValidateParams: data did not match any variant of untagged enum SchemaFileInput at line 1 column 29""
          `)
        }
      })
      test('validation errors', () => {
        expect.assertions(1)
        const schema = `generator client {
          provider = "prisma-client-js"
        }
        
        datasource my_db {
          provider = "sqlite"
          url      = "file:dev.db"
        }
        
        model User {
          id           String     @id @default(cuid())
          id           String     @id @default(cuid())
          name         String
          email        String     @unique
          status       String     @default("")
          permissions  Permission @default()
          permissions  Permission @default("")
          posts        Post[]
          posts        Post[]
        }
        
        model Post {
          id        String   @id @default(cuid())
          name      String
          email     String   @unique
          createdAt DateTime @default(now())
          updatedAt DateTime @updatedAt
        }
        
        enum Permission {
          ADMIN
          USER
          OWNER
          COLLABORATOR
        }
        `
        const schemas: MultipleSchemas = [['schema.prisma', schema]]
        try {
          validate({ schemas })
        } catch (e) {
          expect(stripVTControlCharacters(e.message)).toMatchInlineSnapshot(`
            "Prisma schema validation - (validate wasm)
            Error code: P1012
            error: Field "id" is already defined on model "User".
              -->  schema.prisma:12
               | 
            11 |           id           String     @id @default(cuid())
            12 |           id           String     @id @default(cuid())
               | 
            error: Field "permissions" is already defined on model "User".
              -->  schema.prisma:17
               | 
            16 |           permissions  Permission @default()
            17 |           permissions  Permission @default("")
               | 
            error: Field "posts" is already defined on model "User".
              -->  schema.prisma:19
               | 
            18 |           posts        Post[]
            19 |           posts        Post[]
               | 
            error: Error validating model "User": At most one field must be marked as the id field with the \`@id\` attribute.
              -->  schema.prisma:10
               | 
             9 |         
            10 |         model User {
            11 |           id           String     @id @default(cuid())
            12 |           id           String     @id @default(cuid())
            13 |           name         String
            14 |           email        String     @unique
            15 |           status       String     @default("")
            16 |           permissions  Permission @default()
            17 |           permissions  Permission @default("")
            18 |           posts        Post[]
            19 |           posts        Post[]
            20 |         }
               | 
            error: Argument "value" is missing.
              -->  schema.prisma:16
               | 
            15 |           status       String     @default("")
            16 |           permissions  Permission @default()
               | 
            error: Error parsing attribute "@default": Expected an enum value, but found \`""\`.
              -->  schema.prisma:17
               | 
            16 |           permissions  Permission @default()
            17 |           permissions  Permission @default("")
               | 
            Validation Error Count: 6
            [Context: validate]
            Prisma CLI Version : 0.0.0"
          `)
        }
      })
    })
    describe('multiple files', () => {
      test(`panics when the given datamodel isn't an array of string tuples`, () => {
        expect.assertions(3)
        try {
          // @ts-expect-error
          validate({ schemas: [['schema.prisma', true]] })
        } catch (e) {
          expect(isRustPanic(e)).toBe(true)
          expect(serialize(e.message)).toMatchInlineSnapshot(`
            ""RuntimeError: panicked at prisma-fmt/src/validate.rs:0:0:
            Failed to deserialize ValidateParams: data did not match any variant of untagged enum SchemaFileInput at line 1 column 40""
          `)
          expect(e.rustStack).toBeTruthy()
        }
      })
      test('validation errors', () => {
        expect.assertions(1)
        const datamodel1 = /* prisma */ `
          generator client {
            provider = "prisma-client-js"
          }
          datasource my_db {
            provider = "sqlite"
            url      = "file:dev.db"
          }
          model User {
            id           String     @id @default(cuid())
            id           String     @id @default(cuid())
            name         String
            email        String     @unique
            status       String     @default("")
            permissions  Permission @default()
            permissions  Permission @default("")
            posts        Post[]
            posts        Post[]
          }
        `
        const datamodel2 = /* prisma */ `
          model Post {
            id        String   @id @default(cuid())
            name      String
            email     String   @unique
            createdAt DateTime @default(now())
            updatedAt DateTime @updatedAt
          }
          
          enum Permission {
            ADMIN
            USER
            OWNER
            COLLABORATOR
          }
        `
        const datamodel: SchemaFileInput = [
          ['schema.prisma', datamodel1],
          ['schema2.prisma', datamodel2],
        ]
        try {
          validate({ schemas: datamodel })
        } catch (e) {
          // TODO: patch engines to fix this message, it should group errors by the different filenames.
          expect(stripVTControlCharacters(e.message)).toMatchInlineSnapshot(`
            "Prisma schema validation - (validate wasm)
            Error code: P1012
            error: Field "id" is already defined on model "User".
              -->  schema.prisma:13
               | 
            12 |             id           String     @id @default(cuid())
            13 |             id           String     @id @default(cuid())
               | 
            error: Field "permissions" is already defined on model "User".
              -->  schema.prisma:18
               | 
            17 |             permissions  Permission @default()
            18 |             permissions  Permission @default("")
               | 
            error: Field "posts" is already defined on model "User".
              -->  schema.prisma:20
               | 
            19 |             posts        Post[]
            20 |             posts        Post[]
               | 
            error: Error validating model "User": At most one field must be marked as the id field with the \`@id\` attribute.
              -->  schema.prisma:11
               | 
            10 | 
            11 |           model User {
            12 |             id           String     @id @default(cuid())
            13 |             id           String     @id @default(cuid())
            14 |             name         String
            15 |             email        String     @unique
            16 |             status       String     @default("")
            17 |             permissions  Permission @default()
            18 |             permissions  Permission @default("")
            19 |             posts        Post[]
            20 |             posts        Post[]
            21 |           }
               | 
            error: Argument "value" is missing.
              -->  schema.prisma:17
               | 
            16 |             status       String     @default("")
            17 |             permissions  Permission @default()
               | 
            error: Error parsing attribute "@default": Expected an enum value, but found \`""\`.
              -->  schema.prisma:18
               | 
            17 |             permissions  Permission @default()
            18 |             permissions  Permission @default("")
               | 
            Validation Error Count: 6
            [Context: validate]
            Prisma CLI Version : 0.0.0"
          `)
        }
      })
    })
  })
  describe('success', () => {
    test('simple model, no datasource', () => {
      const schema /* prisma */ = `model A {
        id Int @id
        name String
      }`
      const schemas: MultipleSchemas = [['schema.prisma', schema]]
      validate({ schemas })
    })
    test('simple model, sqlite', () => {
      const schema /* prisma */ = `datasource db {
        provider = "sqlite"
        url      = "file:dev.db"
      }
      model A {
        id Int @id
        name String
      }`
      const schemas: MultipleSchemas = [['schema.prisma', schema]]
      validate({ schemas })
    })
    test('chinook introspected schema', async () => {
      const { schemas } = await getSchemaWithPath(path.join(fixturesPath, 'chinook.prisma'))
      validate({ schemas })
    })
    test('odoo introspected schema', async () => {
      const { schemas } = await getSchemaWithPath(path.join(fixturesPath, 'odoo.prisma'))
      validate({ schemas })
    })
  })
})