Skip to main content
Glama

Prisma MCP Server

Official
by prisma
Apache 2.0
4
44,213
  • Linux
  • Apple
setupTestSuiteEnv.ts15.7 kB
import path from 'node:path' import { Script } from 'node:vm' import { D1Database, D1PreparedStatement, D1Result } from '@cloudflare/workers-types' import { faker } from '@faker-js/faker' import { defaultTestConfig } from '@prisma/config' import { assertNever } from '@prisma/internals' import * as miniProxy from '@prisma/mini-proxy' import { execa } from 'execa' import fs from 'fs-extra' import { match } from 'ts-pattern' import { DbDrop } from '../../../../migrate/src/commands/DbDrop' import { DbExecute } from '../../../../migrate/src/commands/DbExecute' import { DbPush } from '../../../../migrate/src/commands/DbPush' import type { NamedTestSuiteConfig } from './getTestSuiteInfo' import { getTestSuiteFolderPath, getTestSuiteSchemaPath, testSuiteHasTypedSql } from './getTestSuiteInfo' import { AdapterProviders, Providers } from './providers' import type { TestSuiteMeta } from './setupTestSuiteMatrix' import { AlterStatementCallback, ClientMeta } from './types' const DB_NAME_VAR = 'PRISMA_DB_NAME' /** * Copies the necessary files for the generated test suite folder. */ export async function setupTestSuiteFiles({ suiteMeta, suiteConfig, }: { suiteMeta: TestSuiteMeta suiteConfig: NamedTestSuiteConfig }) { const suiteFolder = getTestSuiteFolderPath({ suiteMeta, suiteConfig }) // we copy the minimum amount of files needed for the test suite await fs.copy(path.join(suiteMeta.testRoot, 'prisma'), path.join(suiteFolder, 'prisma')) if (await testSuiteHasTypedSql(suiteMeta)) { await fs.copy(suiteMeta.sqlPath, path.join(suiteFolder, 'prisma', 'sql')) } await fs.mkdir(path.join(suiteFolder, suiteMeta.rootRelativeTestDir), { recursive: true }) await copyPreprocessed({ from: suiteMeta.testPath, to: path.join(suiteFolder, suiteMeta.rootRelativeTestPath), // Default to undefined so that every field gets bound to a variable // when evaluating @ts-test-if magic comments. suiteConfig: { generatorType: undefined, driverAdapter: undefined, relationMode: undefined, engineType: undefined, clientRuntime: undefined, ...suiteConfig.matrixOptions, }, }) } /** * Copies test file into generated subdirectory and pre-processes it * in the following way: * * 1. Adjusts relative imports so they'll work from generated subfolder * 2. Evaluates @ts-test-if magic comments and replaces them with @ts-expect-error * if necessary */ async function copyPreprocessed({ from, to, suiteConfig, }: { from: string to: string suiteConfig: Record<string, unknown> }): Promise<void> { // we adjust the relative paths to work from the generated folder const contents = await fs.readFile(from, 'utf8') let newContents = contents .replace(/'\.\.\//g, "'../../../") .replace(/'\.\//g, "'../../") .replace(/'\.\.\/\.\.\/generated\/prisma\//g, "'./generated/prisma/") .replace(/\/\/\s*@ts-ignore.*/g, '') .replace(/\/\/\s*@ts-test-if:(.+)/g, (match, condition) => { if (!evaluateMagicComment({ conditionFromComment: condition, suiteConfig })) { return '// @ts-expect-error' } return match }) if (suiteConfig['generatorType'] === 'prisma-client-ts') { newContents = newContents.replace(/\/\/\s*@only-js-generator.*\n.*/g, '') } else { newContents = newContents.replace(/\/\/\s*@only-ts-generator.*\n.*/g, '') newContents = newContents.replace(/\/generated\/prisma\/sql/g, '/generated/prisma/client/sql') } await fs.writeFile(to, newContents, 'utf8') } /** * Evaluates the condition from @ts-test-if magic comment as * a JS expression. * All properties from suite config are available as variables * within the expression. */ function evaluateMagicComment({ conditionFromComment, suiteConfig, }: { conditionFromComment: string suiteConfig: Record<string, unknown> }): boolean { const script = new Script(` ${conditionFromComment} `) const value = script.runInNewContext({ ...suiteConfig, Providers, }) return Boolean(value) } /** * Write the generated test suite schema to the test suite folder. */ export async function setupTestSuiteSchema({ suiteMeta, suiteConfig, schema, }: { suiteMeta: TestSuiteMeta suiteConfig: NamedTestSuiteConfig schema: string }) { const schemaPath = getTestSuiteSchemaPath({ suiteMeta, suiteConfig }) await fs.writeFile(schemaPath, schema) } /** * Create a database for the generated schema of the test suite. */ export async function setupTestSuiteDatabase({ suiteMeta, suiteConfig, errors = [], alterStatementCallback, cfWorkerBindings, }: { suiteMeta: TestSuiteMeta suiteConfig: NamedTestSuiteConfig errors?: Error[] alterStatementCallback?: AlterStatementCallback cfWorkerBindings?: { [key: string]: unknown } }) { const schemaPath = getTestSuiteSchemaPath({ suiteMeta, suiteConfig }) const consoleInfoMock = jest.spyOn(console, 'info').mockImplementation() try { if (suiteConfig.matrixOptions.driverAdapter === AdapterProviders.JS_D1) { await setupTestSuiteDatabaseD1({ schemaPath, cfWorkerBindings: cfWorkerBindings!, alterStatementCallback }) } else { const dbPushParams = ['--schema', schemaPath, '--skip-generate'] // we reuse and clean the db when it is explicitly required if (process.env.TEST_REUSE_DATABASE === 'true') { dbPushParams.push('--force-reset') } await DbPush.new().parse(dbPushParams, defaultTestConfig()) if ( suiteConfig.matrixOptions.driverAdapter === AdapterProviders.VITESS_8 || suiteConfig.matrixOptions.driverAdapter === AdapterProviders.JS_PLANETSCALE ) { // wait for vitess to catch up, corresponds to TABLET_REFRESH_INTERVAL in docker-compose.yml await new Promise((r) => setTimeout(r, 1_000)) } } if (alterStatementCallback) { const { provider } = suiteConfig.matrixOptions const prismaDir = path.dirname(schemaPath) const timestamp = new Date().getTime() if (provider === Providers.MONGODB) { throw new Error('DbExecute not supported with mongodb') } await fs.promises.mkdir(`${prismaDir}/migrations/${timestamp}`, { recursive: true }) await fs.promises.writeFile(`${prismaDir}/migrations/migration_lock.toml`, `provider = "${provider}"`) await fs.promises.writeFile( `${prismaDir}/migrations/${timestamp}/migration.sql`, alterStatementCallback(provider), ) await DbExecute.new().parse( ['--file', `${prismaDir}/migrations/${timestamp}/migration.sql`, '--schema', `${schemaPath}`], defaultTestConfig(), ) } consoleInfoMock.mockRestore() } catch (e) { errors.push(e as Error) if (errors.length > 2) { throw new Error(errors.map((e) => `${e.message}\n${e.stack}`).join(`\n`)) } else { await setupTestSuiteDatabase({ suiteMeta, suiteConfig, errors, alterStatementCallback: undefined, cfWorkerBindings, }) // retry logic } } } /** * Cleanup the D1 database and apply the DDL generated from migrate diff for the generated schema of the test suite. * The Schema Engine does not know how to use a Driver Adapter at the moment * So we cannot use `db push` for D1 */ export async function setupTestSuiteDatabaseD1({ schemaPath, cfWorkerBindings, alterStatementCallback, }: { schemaPath: string cfWorkerBindings: { [key: string]: unknown } alterStatementCallback?: AlterStatementCallback }) { // Cleanup the database await prepareD1Database({ cfWorkerBindings }) // Use `migrate diff` to get the DDL statements const diffResult = await execa( '../cli/src/bin.ts', ['migrate', 'diff', '--from-empty', '--to-schema-datamodel', schemaPath, '--script'], { env: { DEBUG: process.env.DEBUG, }, }, ) const sqlStatements = diffResult.stdout const d1Client = cfWorkerBindings.MY_DATABASE as D1Database // Execute the DDL statements for (const sqlStatement of sqlStatements.split(';')) { if (sqlStatement.includes('CREATE ')) { await d1Client.prepare(sqlStatement).run() } else if (sqlStatement === '\n') { // Ignore } else { console.debug(`Skipping ${sqlStatement} as it is not a CREATE statement`) } } if (alterStatementCallback) { const alterSqlStatements = alterStatementCallback(Providers.SQLITE) // Execute the DDL statements for (const alterSqlStatement of alterSqlStatements.split(';')) { if (alterSqlStatement === '\n') { // Ignore } else { await d1Client.prepare(alterSqlStatement).run() } } } } /** * Drop the database for the generated schema of the test suite. */ export async function dropTestSuiteDatabase({ suiteMeta, suiteConfig, errors = [], cfWorkerBindings, }: { suiteMeta: TestSuiteMeta suiteConfig: NamedTestSuiteConfig errors?: Error[] cfWorkerBindings?: { [key: string]: unknown } }) { const schemaPath = getTestSuiteSchemaPath({ suiteMeta, suiteConfig }) if (suiteConfig.matrixOptions.driverAdapter === AdapterProviders.JS_D1) { return await prepareD1Database({ cfWorkerBindings: cfWorkerBindings! }) } try { const consoleInfoMock = jest.spyOn(console, 'info').mockImplementation() await DbDrop.new().parse(['--schema', schemaPath, '--force', '--preview-feature'], defaultTestConfig()) consoleInfoMock.mockRestore() } catch (e) { errors.push(e as Error) if (errors.length > 2) { throw new Error(errors.map((e) => `${e.message}\n${e.stack}`).join(`\n`)) } else { await dropTestSuiteDatabase({ suiteMeta, suiteConfig, errors, cfWorkerBindings }) // retry logic } } } async function prepareD1Database({ cfWorkerBindings }: { cfWorkerBindings: { [key: string]: unknown } }) { const d1Client = cfWorkerBindings.MY_DATABASE as D1Database const existingItems = ((await d1Client.prepare(`PRAGMA main.table_list;`).run()) as D1Result<Record<string, unknown>>) .results for (const item of existingItems) { const batch: D1PreparedStatement[] = [] if (item.name === '_cf_KV' || item.name === 'sqlite_schema') { continue } else if (item.name === 'sqlite_sequence') { batch.push(d1Client.prepare('DELETE FROM `sqlite_sequence`;')) } else if (item.type === 'view') { batch.push(d1Client.prepare(`DROP VIEW "${item.name}";`)) } else { // Check indexes const existingIndexes = ( (await d1Client.prepare(`PRAGMA index_list("${item.name}");`).run()) as D1Result<Record<string, unknown>> ).results const indexesToDrop = existingIndexes.filter((i) => i.origin === 'c') for (const index of indexesToDrop) { batch.push(d1Client.prepare(`DROP INDEX "${index.name}";`)) } // We cannot do `DROP TABLE "${item.name}";` // Because we cannot use "PRAGMA foreign_keys = OFF;" as it is ignored inside transactions // and everything runs inside an implicit transaction on D1 batch.push( d1Client.prepare( `ALTER TABLE "${item.name}" RENAME TO ${(item.name as string).split('_')[0]}_${new Date().getTime()};`, ), ) } const batchResult = await d1Client.batch(batch) // @ts-ignore if (batchResult.error) { // @ts-ignore console.error('Error in batch: %O', batchResult.error) } } } export type DatasourceInfo = { directEnvVarName: string envVarName: string databaseUrl: string accelerateUrl?: string } /** * Generate a random string to be used as a test suite db name, and derive the * corresponding database URL and, if required, Mini-Proxy connection string to * that database. */ export function setupTestSuiteDbURI({ suiteConfig, clientMeta, }: { suiteConfig: NamedTestSuiteConfig['matrixOptions'] clientMeta: ClientMeta }): DatasourceInfo { const { provider, driverAdapter } = suiteConfig const envVarName = `DATABASE_URI_${provider}` const directEnvVarName = `DIRECT_${envVarName}` let databaseUrl = match(driverAdapter) .with(undefined, () => getDbUrl(provider)) .otherwise(() => getDbUrlFromFlavor(driverAdapter, provider)) if (process.env.TEST_REUSE_DATABASE === 'true') { // we reuse and clean the same db when running in single-threaded mode databaseUrl = databaseUrl.replace(DB_NAME_VAR, 'test-0000-00000000') } else { const dbId = `${faker.string.alphanumeric(5)}-${process.pid}-${Date.now()}` databaseUrl = databaseUrl.replace(DB_NAME_VAR, dbId) } let accelerateUrl: string | undefined if (clientMeta.dataProxy) { accelerateUrl = miniProxy.generateConnectionString({ databaseUrl, envVar: envVarName, port: miniProxy.defaultServerConfig.port, }) } return { directEnvVarName, envVarName, databaseUrl, accelerateUrl, } } /** * Returns configured database URL for specified provider * @param provider * @returns */ function getDbUrl(provider: Providers): string { switch (provider) { case Providers.SQLITE: return `file:${DB_NAME_VAR}.db` case Providers.MONGODB: return requireEnvVariable('TEST_FUNCTIONAL_MONGO_URI') case Providers.POSTGRESQL: return requireEnvVariable('TEST_FUNCTIONAL_POSTGRES_URI') case Providers.MYSQL: return requireEnvVariable('TEST_FUNCTIONAL_MYSQL_URI') case Providers.COCKROACHDB: return requireEnvVariable('TEST_FUNCTIONAL_COCKROACH_URI') case Providers.SQLSERVER: return requireEnvVariable('TEST_FUNCTIONAL_MSSQL_URI') default: return assertNever(provider, `No URL for provider ${provider} configured`) } } /** * Returns configured database URL for specified provider, Driver Adapter, or provider variant (e.g., Vitess 8 is a known variant of the "mysql" provider), * falling back to `getDbUrl(provider)` if no specific URL is configured. * @param driverAdapter provider variant, e.g. `vitess` for `mysql` * @param provider provider supported by Prisma, e.g. `mysql` */ function getDbUrlFromFlavor(driverAdapterOrFlavor: `${AdapterProviders}` | undefined, provider: Providers): string { return ( match(driverAdapterOrFlavor) .with(AdapterProviders.VITESS_8, () => requireEnvVariable('TEST_FUNCTIONAL_VITESS_8_URI')) // Note: we're using Postgres 10 for Postgres (Rust driver, `pg` driver adapter), // and Postgres 16 for Neon due to https://github.com/prisma/team-orm/issues/511. .with(AdapterProviders.JS_PG, () => requireEnvVariable('TEST_FUNCTIONAL_POSTGRES_URI')) .with(AdapterProviders.JS_NEON, () => requireEnvVariable('TEST_FUNCTIONAL_POSTGRES_16_URI')) .with(AdapterProviders.JS_PLANETSCALE, () => requireEnvVariable('TEST_FUNCTIONAL_VITESS_8_URI')) .with(AdapterProviders.JS_LIBSQL, () => requireEnvVariable('TEST_FUNCTIONAL_LIBSQL_FILE_URI')) .with(AdapterProviders.JS_BETTER_SQLITE3, () => requireEnvVariable('TEST_FUNCTIONAL_BETTER_SQLITE3_FILE_URI')) .otherwise(() => getDbUrl(provider)) ) } /** * Gets the value of environment variable or throws error if it is not set * @param varName * @returns */ function requireEnvVariable(varName: string): string { const value = process.env[varName] if (!value) { throw new Error( `Required env variable ${varName} is not set. See https://github.com/prisma/prisma/blob/main/TESTING.md for instructions`, ) } if (!value.includes(DB_NAME_VAR)) { throw new Error( `Env variable ${varName} must include ${DB_NAME_VAR} placeholder. See https://github.com/prisma/prisma/blob/main/TESTING.md for instructions`, ) } return value }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/prisma/prisma'

If you have feedback or need assistance with the MCP directory API, please join our Discord server