import { Debug } from '@prisma/debug'
import type {
GetSchemaResult,
LookupResult,
NonFatalLookupError,
SuccessfulLookupResult,
} from '@prisma/schema-files-loader'
import { ensureType, loadSchemaFiles } from '@prisma/schema-files-loader'
import fs from 'fs'
import { dim, green } from 'kleur/colors'
import path from 'path'
import { promisify } from 'util'
import type { MultipleSchemaTuple } from '../utils/schemaFileInput'
const readFile = promisify(fs.readFile)
const stat = promisify(fs.stat)
const debug = Debug('prisma:getSchema')
type DefaultLookupRuleFailure = {
path: string
error: NonFatalLookupError
}
type DefaultLookupError = {
kind: 'NotFoundMultipleLocations'
failures: DefaultLookupRuleFailure[]
}
type DefaultLookupResult =
| SuccessfulLookupResult
| {
ok: false
error: DefaultLookupError
}
export type GetSchemaOptions = {
schemaPath: SchemaPathInput
cwd?: string
argumentName?: string
}
type GetSchemaInternalOptions = Required<GetSchemaOptions>
/**
* Creates SchemaPathInput based on a combination of possible inputs
* from CLI args, config file, or base directory.
* `baseDir` is either the directory containing the prisma config file or the working directory
* of the CLI invocation if no config file is found.
*/
export function createSchemaPathInput({
schemaPathFromArgs,
schemaPathFromConfig,
baseDir,
}: {
schemaPathFromArgs?: string
schemaPathFromConfig?: string
baseDir: string
}): SchemaPathInput {
return schemaPathFromArgs
? { cliProvidedPath: schemaPathFromArgs }
: schemaPathFromConfig
? { configProvidedPath: schemaPathFromConfig }
: { baseDir }
}
/**
* Loads the schema, throws an error if it is not found
*/
export async function getSchemaWithPath({
schemaPath,
cwd = process.cwd(),
argumentName = '--schema',
}: GetSchemaOptions): Promise<GetSchemaResult> {
const result = await getSchemaWithPathInternal({ schemaPath, cwd, argumentName })
if (result.ok) {
return result.schema
}
throw new Error(renderDefaultLookupError(result.error, cwd))
}
/**
* The schema path can be provided as a CLI argument, a configuration file, or a base directory
* that is expected to contain the schema in one of the default locations.
*/
export type SchemaPathInput = { cliProvidedPath: string } | { configProvidedPath: string } | { baseDir: string }
/**
* Loads the schema, returns null if it is not found
* Throws an error if schema is specified explicitly in
* any of the available ways (argument, package.json config), but
* can not be loaded
* @param schemaPathFromArgs
* @param schemaPathFromConfig
* @param opts
* @returns
*/
export async function getSchemaWithPathOptional({
schemaPath,
cwd = process.cwd(),
argumentName = '--schema',
}: GetSchemaOptions): Promise<GetSchemaResult | null> {
const result = await getSchemaWithPathInternal({ schemaPath, cwd, argumentName })
if (result.ok) {
return result.schema
}
return null
}
export function printSchemaLoadedMessage(schemaPath: string) {
// TODO: this causes https://github.com/prisma/prisma/issues/27005
process.stdout.write(dim(`Prisma schema loaded from ${path.relative(process.cwd(), schemaPath)}`) + '\n')
}
async function readSchemaFromSingleFile(schemaPath: string): Promise<LookupResult> {
debug('Reading schema from single file', schemaPath)
const typeError = await ensureType(schemaPath, 'file')
if (typeError) {
return { ok: false, error: typeError }
}
const file = await readFile(schemaPath, { encoding: 'utf-8' })
const schemaTuple: MultipleSchemaTuple = [schemaPath, file]
return {
ok: true,
schema: { schemaPath, schemaRootDir: path.dirname(schemaPath), schemas: [schemaTuple] },
} as const
}
async function readSchemaFromDirectory(schemaPath: string): Promise<LookupResult> {
debug('Reading schema from multiple files', schemaPath)
const typeError = await ensureType(schemaPath, 'directory')
if (typeError) {
return { ok: false, error: typeError }
}
const files = await loadSchemaFiles(schemaPath)
return { ok: true, schema: { schemaPath, schemaRootDir: schemaPath, schemas: files } }
}
async function readSchemaFromFileOrDirectory(schemaPath: string): Promise<LookupResult> {
let stats: fs.Stats
try {
stats = await stat(schemaPath)
} catch (e) {
if (e.code === 'ENOENT') {
return { ok: false, error: { kind: 'NotFound', path: schemaPath } }
}
throw e
}
if (stats.isFile()) {
return readSchemaFromSingleFile(schemaPath)
}
if (stats.isDirectory()) {
return readSchemaFromDirectory(schemaPath)
}
return { ok: false, error: { kind: 'WrongType', path: schemaPath, expectedTypes: ['file', 'directory'] } }
}
/**
* Tries to load schema from either provided
* arg, prisma.config.ts location, default location relative to cwd
* or any of the Yarn1Workspaces.
*
* If schema is specified explicitly with any of the methods but can
* not be loaded, error will be thrown. If no explicit schema is given, then
* error value will be returned instead
*/
async function getSchemaWithPathInternal({
schemaPath,
cwd,
argumentName,
}: GetSchemaInternalOptions): Promise<DefaultLookupResult> {
// 1. Try the user custom path, when provided.
if ('cliProvidedPath' in schemaPath) {
return {
ok: true,
schema: await getCliProvidedSchemaFile(schemaPath.cliProvidedPath, cwd, argumentName),
}
}
// 2. Try the `schema` from `PrismaConfig`
if ('configProvidedPath' in schemaPath) {
return {
ok: true,
schema: await getConfigProvidedSchemaFile(schemaPath.configProvidedPath),
}
}
// 3. Look into the default, "canonical" locations in the cwd (e.g., `./schema.prisma` or `./prisma/schema.prisma`)
const defaultResult = await getDefaultSchema(schemaPath.baseDir)
if (defaultResult.ok) {
return defaultResult
}
return {
ok: false as const,
error: defaultResult.error,
}
}
function renderLookupError(error: NonFatalLookupError) {
switch (error.kind) {
case 'NotFound': {
const expected = error.expectedType ?? 'file or directory'
return `${expected} not found`
}
case 'WrongType':
return `expected ${error.expectedTypes.join(' or ')}`
}
}
function renderDefaultLookupError(error: DefaultLookupError, cwd: string) {
const parts: string[] = [
`Could not find Prisma Schema that is required for this command.`,
`You can either provide it with ${green('`--schema`')} argument,`,
`set it in your Prisma Config file (e.g., ${green('`prisma.config.ts`')}),`,
`set it as ${green('`prisma.schema`')} in your ${green('package.json')},`,
`or put it into the default location (${green('`./prisma/schema.prisma`')}, or ${green('`./schema.prisma`')}.`,
'Checked following paths:\n',
]
const printedPaths = new Set<string>()
for (const failure of error.failures) {
const filePath = failure.path
if (!printedPaths.has(failure.path)) {
parts.push(`${path.relative(cwd, filePath)}: ${renderLookupError(failure.error)}`)
printedPaths.add(filePath)
}
}
parts.push('\nSee also https://pris.ly/d/prisma-schema-location')
return parts.join('\n')
}
export async function getCliProvidedSchemaFile(
schemaPathFromArgs: string,
cwd: string = process.cwd(),
argumentName: string = '--schema',
): Promise<GetSchemaResult> {
const absPath = path.resolve(cwd, schemaPathFromArgs)
const customSchemaResult = await readSchemaFromFileOrDirectory(absPath)
if (!customSchemaResult.ok) {
const relPath = path.relative(cwd, absPath)
throw new Error(
`Could not load \`${argumentName}\` from provided path \`${relPath}\`: ${renderLookupError(
customSchemaResult.error,
)}`,
)
}
return customSchemaResult.schema
}
export async function getConfigProvidedSchemaFile(schemaPathFromConfig: string): Promise<GetSchemaResult> {
const schemaResult = await readSchemaFromFileOrDirectory(schemaPathFromConfig)
if (!schemaResult.ok) {
throw new Error(
`Could not load schema from \`${schemaPathFromConfig}\` provided by "prisma.config.ts"\`: ${renderLookupError(
schemaResult.error,
)}`,
)
}
return schemaResult.schema
}
async function getDefaultSchema(cwd: string, failures: DefaultLookupRuleFailure[] = []): Promise<DefaultLookupResult> {
const lookupPaths = [path.join(cwd, 'schema.prisma'), path.join(cwd, 'prisma', 'schema.prisma')]
for (const path of lookupPaths) {
debug(`Checking existence of ${path}`)
const schema = await readSchemaFromSingleFile(path)
if (!schema.ok) {
failures.push({ path, error: schema.error })
continue
}
return schema
}
return {
ok: false,
error: {
kind: 'NotFoundMultipleLocations',
failures,
},
}
}