diff --git a/.changeset/fix-keystone-prisma.md b/.changeset/fix-keystone-prisma.md new file mode 100644 index 00000000000..4b71b10fec5 --- /dev/null +++ b/.changeset/fix-keystone-prisma.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': patch +--- + +Fix `keystone prisma ...` not returning the same error code as the Prisma engine diff --git a/packages/core/package.json b/packages/core/package.json index 101e25d11da..74f8ffe0e07 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -204,9 +204,9 @@ "@keystone-ui/toast": "workspace:^", "@keystone-ui/tooltip": "workspace:^", "@nodelib/fs.walk": "^2.0.0", - "@prisma/client": "5.14.0", - "@prisma/internals": "5.14.0", - "@prisma/migrate": "5.14.0", + "@prisma/client": "5.15.1", + "@prisma/internals": "5.15.1", + "@prisma/migrate": "5.15.1", "@sindresorhus/slugify": "^1.1.2", "apollo-upload-client": "^17.0.0", "bcryptjs": "^2.4.3", @@ -235,7 +235,7 @@ "meow": "^9.0.0", "next": "^14.2.0", "pluralize": "^8.0.0", - "prisma": "5.14.0", + "prisma": "5.15.1", "prompts": "^2.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/artifacts.ts b/packages/core/src/___internal-do-not-use-will-break-in-patch/artifacts.ts index f9a061eb716..c9081a813c2 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/artifacts.ts +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/artifacts.ts @@ -5,9 +5,12 @@ export { createSystem, } from '../lib/createSystem' export { - pushPrismaSchemaToDatabase, + withMigrate } from '../lib/migrations' export { generateArtifacts, getArtifacts, } from '../artifacts' +export { + ExitError +} from '../scripts/utils' diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index d9cad476d18..8dff423ab59 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -2,7 +2,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import { type ChildProcess } from 'node:child_process' -import { printSchema, type GraphQLSchema } from 'graphql' +import { printSchema } from 'graphql' import { getGenerators, formatSchema } from '@prisma/internals' import { ExitError } from './scripts/utils' import { type __ResolvedKeystoneConfig } from './types' @@ -39,7 +39,7 @@ export async function validateArtifacts ( system: System, ) { const paths = system.getPaths(cwd) - const artifacts = await getCommittedArtifacts(system.config, system.graphQLSchema) + const artifacts = await getArtifacts(system) const [writtenGraphQLSchema, writtenPrismaSchema] = await Promise.all([ readFileButReturnNothingIfDoesNotExist(paths.schema.graphql), readFileButReturnNothingIfDoesNotExist(paths.schema.prisma), @@ -67,22 +67,23 @@ export async function validateArtifacts ( throw new ExitError(1) } -async function getCommittedArtifacts (config: __ResolvedKeystoneConfig, graphQLSchema: GraphQLSchema) { - const lists = initialiseLists(config) - const prismaSchema = printPrismaSchema(config, lists) +export async function getArtifacts (system: System) { + const lists = initialiseLists(system.config) + const prismaSchema = await formatSchema({ + schemas: [ + [system.config.db.prismaSchemaPath, printPrismaSchema(system.config, lists)] + ] + }) + return { - graphql: getFormattedGraphQLSchema(printSchema(graphQLSchema)), - prisma: (await formatSchema({ schemas: [[config.db.prismaSchemaPath, prismaSchema]] }))[0][1], + graphql: getFormattedGraphQLSchema(printSchema(system.graphQLSchema)), + prisma: prismaSchema[0][1], } } -export async function getArtifacts (system: System) { - return await getCommittedArtifacts(system.config, system.graphQLSchema) -} - export async function generateArtifacts (cwd: string, system: System) { const paths = getSystemPaths(cwd, system.config) - const artifacts = await getCommittedArtifacts(system.config, system.graphQLSchema) + const artifacts = await getArtifacts(system) await fs.writeFile(paths.schema.graphql, artifacts.graphql) await fs.writeFile(paths.schema.prisma, artifacts.prisma) return artifacts diff --git a/packages/core/src/lib/createSystem.ts b/packages/core/src/lib/createSystem.ts index 31f8f3137e5..6dcfae09eff 100644 --- a/packages/core/src/lib/createSystem.ts +++ b/packages/core/src/lib/createSystem.ts @@ -12,17 +12,16 @@ import { resolveDefaults } from './defaults' import { createAdminMeta } from './create-admin-meta' import { createGraphQLSchema } from './createGraphQLSchema' import { createContext } from './context/createContext' -import { initialiseLists, type InitialisedList } from './core/initialise-lists' +import { + type InitialisedList, + initialiseLists, +} from './core/initialise-lists' // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig export function getBuiltKeystoneConfigurationPath (cwd: string) { return path.join(cwd, '.keystone/config.js') } -export function getBuiltKeystoneConfiguration (cwd: string) { - return require(getBuiltKeystoneConfigurationPath(cwd)).default -} - function posixify (s: string) { return s.split(path.sep).join('/') } diff --git a/packages/core/src/lib/migrations.ts b/packages/core/src/lib/migrations.ts index a2dd75d66ff..edfff8a3a2d 100644 --- a/packages/core/src/lib/migrations.ts +++ b/packages/core/src/lib/migrations.ts @@ -1,190 +1,72 @@ import { type ChildProcess } from 'node:child_process' -import path from 'node:path' +import { toSchemasContainer } from '@prisma/internals' -import chalk from 'chalk' -import { createDatabase, uriToCredentials, type DatabaseCredentials } from '@prisma/internals' +// @ts-expect-error import { Migrate } from '@prisma/migrate' import { type System } from './createSystem' -import { ExitError } from '../scripts/utils' -import { confirmPrompt } from './prompts' - -// we don't want to pollute process.env.DATABASE_URL so we're -// setting the env variable _just_ long enough for Migrate to -// read it and then we reset it immediately after. -// Migrate reads the env variables a single time when it starts the child process that it talks to - -// note that we could only run this once per Migrate instance but we're going to do it consistently for all migrate calls -// so that calls can moved around freely without implictly relying on some other migrate command being called before it +function setOrRemoveEnvVariable (name: string, value: string | undefined) { + if (value === undefined) { + delete process.env[name] + return + } + process.env[name] = value +} -// We also want to silence messages from Prisma about available updates, since the developer is -// not in control of their Prisma version. -// https://www.prisma.io/docs/reference/api-reference/environment-variables-reference#prisma_hide_update_message -function runMigrateWithDbUrl ( +export async function withMigrate ( + prismaSchemaPath: string, system: { config: { db: Pick } }, - cb: () => T -): T { - const prevDBURLFromEnv = process.env.DATABASE_URL - const prevShadowDBURLFromEnv = process.env.SHADOW_DATABASE_URL - const prevHiddenUpdateMessage = process.env.PRISMA_HIDE_UPDATE_MESSAGE - try { - process.env.DATABASE_URL = system.config.db.url - setOrRemoveEnvVariable('SHADOW_DATABASE_URL', system.config.db.shadowDatabaseUrl) - process.env.PRISMA_HIDE_UPDATE_MESSAGE = '1' - return cb() - } finally { - setOrRemoveEnvVariable('DATABASE_URL', prevDBURLFromEnv) - setOrRemoveEnvVariable('SHADOW_DATABASE_URL', prevShadowDBURLFromEnv) - setOrRemoveEnvVariable('PRISMA_HIDE_UPDATE_MESSAGE', prevHiddenUpdateMessage) - } -} - -function setOrRemoveEnvVariable (name: string, value: string | undefined) { - if (value === undefined) { - delete process.env[name] - } else { - process.env[name] = value + cb: (operations: { + apply: () => Promise + diagnostic: () => Promise + push: (force: boolean) => Promise + reset: () => Promise + schema: (_: string, force: boolean) => Promise + }) => Promise +) { + const migrate = new Migrate(prismaSchemaPath) + function run (f: () => T): T { + // only required once - on child process start - but easiest to do this always + const prevDBURLFromEnv = process.env.DATABASE_URL + const prevShadowDBURLFromEnv = process.env.SHADOW_DATABASE_URL + const prevHiddenUpdateMessage = process.env.PRISMA_HIDE_UPDATE_MESSAGE + try { + process.env.DATABASE_URL = system.config.db.url + setOrRemoveEnvVariable('SHADOW_DATABASE_URL', system.config.db.shadowDatabaseUrl) + process.env.PRISMA_HIDE_UPDATE_MESSAGE = '1' // temporarily silence + return f() + } finally { + setOrRemoveEnvVariable('DATABASE_URL', prevDBURLFromEnv) + setOrRemoveEnvVariable('SHADOW_DATABASE_URL', prevShadowDBURLFromEnv) + setOrRemoveEnvVariable('PRISMA_HIDE_UPDATE_MESSAGE', prevHiddenUpdateMessage) + } } -} -async function withMigrate (schemaPath: string, cb: (migrate: Migrate) => Promise) { - const migrate = new Migrate(schemaPath) try { - return await cb(migrate) + return await cb({ + async apply () { return run(() => migrate.applyMigrations()) }, + async diagnostic () { return run(() => migrate.devDiagnostic()) }, + async push (force) { return run(() => migrate.push({ force })) }, + async reset () { return run(() => migrate.reset()) }, + async schema (schema, force) { + const schemaContainer = toSchemasContainer([ + [prismaSchemaPath, schema] + ]) + + return run(() => migrate.engine.schemaPush({ force, schema: schemaContainer })) + } + }) } finally { const closePromise = new Promise(resolve => { - const child = (migrate.engine as any).child as ChildProcess + const { child } = migrate.engine as { child: ChildProcess } child.once('exit', () => resolve()) }) migrate.stop() await closePromise } } - -export async function runMigrationsOnDatabase (cwd: string, system: System) { - const paths = system.getPaths(cwd) - return await withMigrate(paths.schema.prisma, async (migrate) => { - const { appliedMigrationNames } = await runMigrateWithDbUrl(system, () => migrate.applyMigrations()) - return appliedMigrationNames - }) -} - -export async function runMigrationsOnDatabaseMaybeReset (cwd: string, system: System) { - const paths = system.getPaths(cwd) - - return await withMigrate(paths.schema.prisma, async (migrate) => { - const diagnostic = await runMigrateWithDbUrl(system, () => migrate.devDiagnostic()) - - if (diagnostic.action.tag === 'reset') { - console.log(diagnostic.action.reason) - const consent = await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}`) - if (!consent) throw new ExitError(1) - - await runMigrateWithDbUrl(system, () => migrate.reset()) - } - - const { appliedMigrationNames } = await runMigrateWithDbUrl(system, () => migrate.applyMigrations()) - return appliedMigrationNames - }) -} - -export async function resetDatabase (dbUrl: string, prismaSchemaPath: string) { - await createDatabase(dbUrl, path.dirname(prismaSchemaPath)) - const config = { - db: { - url: dbUrl, - shadowDatabaseUrl: '' - } - } - - await withMigrate(prismaSchemaPath, async (migrate) => { - await runMigrateWithDbUrl({ config }, () => migrate.reset()) - await runMigrateWithDbUrl({ config }, () => migrate.push({ force: true })) - }) -} - -export async function pushPrismaSchemaToDatabase ( - cwd: string, - system: System, - prismaSchema: string, // already exists - interactive: boolean = false -) { - const paths = system.getPaths(cwd) - - const created = await createDatabase(system.config.db.url, path.dirname(paths.schema.prisma)) - if (interactive && created) { - const credentials = uriToCredentials(system.config.db.url) - console.log(`✨ ${credentials.type} database "${credentials.database}" created at ${getDbLocation(credentials)}`) - } - - const migration = await withMigrate(paths.schema.prisma, async migrate => { - // what does force on migrate.engine.schemaPush mean? - // - true: ignore warnings, but unexecutable steps will block - // - false: warnings or unexecutable steps will block - const migration = await runMigrateWithDbUrl(system, () => migrate.engine.schemaPush({ force: false, schema: prismaSchema })) - - // if there are unexecutable steps, we need to reset the database [or the user can use migrations] - if (migration.unexecutable.length) { - if (!interactive) throw new ExitError(1) - - logUnexecutableSteps(migration.unexecutable) - if (migration.warnings.length) logWarnings(migration.warnings) - - console.log('\nTo apply this migration, we need to reset the database') - if (!(await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}`, false))) { - console.log('Reset cancelled') - throw new ExitError(0) - } - - await runMigrateWithDbUrl(system, () => migrate.reset()) - return runMigrateWithDbUrl(system, () => migrate.engine.schemaPush({ force: false, schema: prismaSchema })) - } - - if (migration.warnings.length) { - if (!interactive) throw new ExitError(1) - - logWarnings(migration.warnings) - if (!(await confirmPrompt(`Do you want to continue? ${chalk.red('Some data will be lost')}`, false))) { - console.log('Push cancelled') - throw new ExitError(0) - } - return runMigrateWithDbUrl(system, () => migrate.engine.schemaPush({ force: true, schema: prismaSchema })) - } - - return migration - }) - - if (!interactive) return - if (migration.warnings.length === 0 && migration.executedSteps === 0) { - console.log(`✨ Database unchanged`) - } else { - console.log(`✨ Database synchronized with Prisma schema`) - } -} - -function logUnexecutableSteps (unexecutableSteps: string[]) { - console.log(`${chalk.bold.red('\n⚠️ We found changes that cannot be executed:\n')}`) - for (const item of unexecutableSteps) { - console.log(` • ${item}`) - } -} - -function logWarnings (warnings: string[]) { - console.warn(chalk.bold(`\n⚠️ Warnings:\n`)) - for (const warning of warnings) { - console.warn(` • ${warning}`) - } -} - -function getDbLocation (credentials: DatabaseCredentials): string { - if (credentials.type === 'sqlite') { - return credentials.uri! - } - - return `${credentials.host}${credentials.port === undefined ? '' : `:${credentials.port}`}` -} diff --git a/packages/core/src/scripts/build.ts b/packages/core/src/scripts/build.ts index 53b77770c03..4c45c1ae1e0 100644 --- a/packages/core/src/scripts/build.ts +++ b/packages/core/src/scripts/build.ts @@ -3,7 +3,6 @@ import nextBuild from 'next/dist/build' import { generateAdminUI } from '../admin-ui/system' import { createSystem, - getBuiltKeystoneConfiguration } from '../lib/createSystem' import { generateArtifacts, @@ -13,16 +12,16 @@ import { } from '../artifacts' import { getEsbuildConfig } from '../lib/esbuild' import type { Flags } from './cli' +import { importBuiltKeystoneConfiguration } from './utils' export async function build ( cwd: string, { frozen, prisma, ui }: Pick ) { + // TODO: should this happen if frozen? await esbuild.build(getEsbuildConfig(cwd)) - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig - const system = createSystem(getBuiltKeystoneConfiguration(cwd)) - + const system = createSystem(await importBuiltKeystoneConfiguration(cwd)) if (prisma) { if (frozen) { await validateArtifacts(cwd, system) diff --git a/packages/core/src/scripts/dev.ts b/packages/core/src/scripts/dev.ts index 0dfc664e04e..b735999ebf5 100644 --- a/packages/core/src/scripts/dev.ts +++ b/packages/core/src/scripts/dev.ts @@ -1,39 +1,38 @@ import fsp from 'node:fs/promises' import path from 'node:path' -import type { ListenOptions } from 'node:net' import url from 'node:url' import { createServer } from 'node:http' -import next from 'next' +import { type ListenOptions } from 'node:net' + +import chalk from 'chalk' +import esbuild, { type BuildResult } from 'esbuild' import express from 'express' +import next from 'next' import { printSchema } from 'graphql' -import esbuild, { type BuildResult } from 'esbuild' +import { createDatabase } from '@prisma/internals' + import { generateAdminUI } from '../admin-ui/system' -import { - pushPrismaSchemaToDatabase, -} from '../lib/migrations' -import { - createSystem, - getBuiltKeystoneConfiguration, -} from '../lib/createSystem' +import { withMigrate } from '../lib/migrations' +import { confirmPrompt } from '../lib/prompts' +import { createSystem, } from '../lib/createSystem' import { getEsbuildConfig } from '../lib/esbuild' import { createExpressServer } from '../lib/createExpressServer' import { createAdminUIMiddlewareWithNextApp } from '../lib/createAdminUIMiddleware' import { runTelemetry } from '../lib/telemetry' import { - getFormattedGraphQLSchema, generateArtifacts, + generatePrismaClient, generateTypes, - generatePrismaClient + getFormattedGraphQLSchema, } from '../artifacts' -import { - type KeystoneConfig -} from '../types' +import { type KeystoneConfig } from '../types' import { printPrismaSchema } from '../lib/core/prisma-schema-printer' import { pkgDir } from '../pkg-dir' -import { ExitError } from './utils' import { - type Flags -} from './cli' + ExitError, + importBuiltKeystoneConfiguration, +} from './utils' +import { type Flags } from './cli' const devLoadingHTMLFilepath = path.join(pkgDir, 'static', 'dev-loading.html') @@ -126,7 +125,7 @@ export async function dev ( if (exit) throw new ExitError(1) } - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig + const app = server ? express() : null const httpServer = app ? createServer(app) : null let expressServer: express.Express | null = null @@ -134,7 +133,7 @@ export async function dev ( const isReady = () => !server || (expressServer !== null && hasAddedAdminUIMiddleware) const initKeystone = async () => { - const configWithExtendHttp = getBuiltKeystoneConfiguration(cwd) + const configWithExtendHttp = await importBuiltKeystoneConfiguration(cwd) const { system, context, @@ -161,7 +160,64 @@ export async function dev ( const paths = system.getPaths(cwd) if (dbPush) { - await pushPrismaSchemaToDatabase(cwd, system, generatedPrismaSchema, true /* interactive */) + const created = await createDatabase(system.config.db.url, path.dirname(paths.schema.prisma)) + if (created) console.log(`✨ database created`) + + const migration = await withMigrate(paths.schema.prisma, system, async (m) => { + // what does force on migrate.engine.schemaPush mean? + // - true: ignore warnings, but unexecutable steps will block + // - false: warnings or unexecutable steps will block + const migration_ = await m.schema(generatedPrismaSchema, false) + + // if there are unexecutable steps, we need to reset the database [or the user can use migrations] + if (migration_.unexecutable.length) { + console.log(`${chalk.bold.red('\n⚠️ We found changes that cannot be executed:\n')}`) + for (const item of migration_.unexecutable) { + console.log(` • ${item}`) + } + + if (migration_.warnings.length) { + console.warn(chalk.bold(`\n⚠️ Warnings:\n`)) + for (const warning of migration_.warnings) { + console.warn(` • ${warning}`) + } + } + + console.log('\nTo apply this migration, we need to reset the database') + if (!(await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}`, false))) { + console.log('Reset cancelled') + throw new ExitError(0) + } + + await m.reset() + return m.schema(generatedPrismaSchema, false) + } + + if (migration_.warnings.length) { + if (migration_.warnings.length) { + console.warn(chalk.bold(`\n⚠️ Warnings:\n`)) + for (const warning of migration_.warnings) { + console.warn(` • ${warning}`) + } + } + + if (!(await confirmPrompt(`Do you want to continue? ${chalk.red('Some data will be lost')}`, false))) { + console.log('Push cancelled') + throw new ExitError(0) + } + + return m.schema(generatedPrismaSchema, true) + } + + return migration_ + }) + + if (migration.warnings.length === 0 && migration.executedSteps === 0) { + console.log(`✨ Database unchanged`) + } else { + console.log(`✨ Database synchronized with Prisma schema`) + } + } else { console.warn('⚠️ Skipping database schema push') } @@ -247,7 +303,7 @@ export async function dev ( delete require.cache[resolved] } - const newConfigWithHttp = getBuiltKeystoneConfiguration(cwd) + const newConfigWithHttp = await importBuiltKeystoneConfiguration(cwd) const newSystem = createSystem(stripExtendHttpServer(newConfigWithHttp)) if (prisma) { @@ -309,7 +365,7 @@ export async function dev ( }) if (app && httpServer) { - const config = getBuiltKeystoneConfiguration(cwd) + const config = await importBuiltKeystoneConfiguration(cwd) app.use('/__keystone/dev/status', (req, res) => { res.status(isReady() ? 200 : 501).end() diff --git a/packages/core/src/scripts/migrate.ts b/packages/core/src/scripts/migrate.ts index e4997c8602d..2447e9c5227 100644 --- a/packages/core/src/scripts/migrate.ts +++ b/packages/core/src/scripts/migrate.ts @@ -1,15 +1,17 @@ -import esbuild from 'esbuild' -import fse from 'fs-extra' import { join } from 'node:path' import { spawn } from 'node:child_process' -import { - createSystem, - getBuiltKeystoneConfiguration -} from '../lib/createSystem' +import chalk from 'chalk' +import esbuild from 'esbuild' +import fse from 'fs-extra' + +import { createSystem } from '../lib/createSystem' import { getEsbuildConfig } from '../lib/esbuild' -import { runMigrationsOnDatabaseMaybeReset } from '../lib/migrations' -import { textPrompt } from '../lib/prompts' +import { withMigrate } from '../lib/migrations' +import { + confirmPrompt, + textPrompt +} from '../lib/prompts' import { generateArtifacts, @@ -18,7 +20,10 @@ import { validateArtifacts, } from '../artifacts' import { type Flags } from './cli' -import { ExitError } from './utils' +import { + ExitError, + importBuiltKeystoneConfiguration, +} from './utils' export async function spawnPrisma (cwd: string, system: { config: { @@ -53,9 +58,7 @@ export async function migrateCreate ( ) { await esbuild.build(getEsbuildConfig(cwd)) - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig - const system = createSystem(getBuiltKeystoneConfiguration(cwd)) - + const system = createSystem(await importBuiltKeystoneConfiguration(cwd)) if (frozen) { await validateArtifacts(cwd, system) console.log('✨ GraphQL and Prisma schemas are up to date') @@ -124,11 +127,10 @@ export async function migrateApply ( cwd: string, { frozen }: Pick ) { + // TODO: should this happen if frozen? await esbuild.build(getEsbuildConfig(cwd)) - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig - const system = createSystem(getBuiltKeystoneConfiguration(cwd)) - + const system = createSystem(await importBuiltKeystoneConfiguration(cwd)) if (frozen) { await validateArtifacts(cwd, system) console.log('✨ GraphQL and Prisma schemas are up to date') @@ -141,6 +143,20 @@ export async function migrateApply ( await generatePrismaClient(cwd, system) console.log('✨ Applying any database migrations') - const migrations = await runMigrationsOnDatabaseMaybeReset(cwd, system) - console.log(migrations.length === 0 ? `✨ No database migrations to apply` : `✨ Database migrated`) + const paths = system.getPaths(cwd) + const { appliedMigrationNames } = await withMigrate(paths.schema.prisma, system, async (m) => { + const diagnostic = await m.diagnostic() + + if (diagnostic.action.tag === 'reset') { + console.log(diagnostic.action.reason) + const consent = await confirmPrompt(`Do you want to continue? ${chalk.red('All data will be lost')}`) + if (!consent) throw new ExitError(1) + + await m.reset() + } + + return await m.apply() + }) + + console.log(appliedMigrationNames.length === 0 ? `✨ No database migrations to apply` : `✨ Database migrated`) } diff --git a/packages/core/src/scripts/prisma.ts b/packages/core/src/scripts/prisma.ts index b2fffb6f37c..a200eba238b 100644 --- a/packages/core/src/scripts/prisma.ts +++ b/packages/core/src/scripts/prisma.ts @@ -1,46 +1,46 @@ -import fs from 'node:fs/promises' import { spawn } from 'node:child_process' -import { - createSystem, - getBuiltKeystoneConfigurationPath, - getBuiltKeystoneConfiguration, -} from '../lib/createSystem' -import { - validateArtifacts, -} from '../artifacts' -import { ExitError } from './utils' -export async function prisma (cwd: string, args: string[], frozen: boolean) { - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig - const builtConfigPath = getBuiltKeystoneConfigurationPath(cwd) +import { createSystem } from '../lib/createSystem' +import { validateArtifacts } from '../artifacts' +import { + ExitError, + importBuiltKeystoneConfiguration, +} from './utils' - // this is the compiled version of the configuration which was generated during the build step - if (!(await fs.stat(builtConfigPath).catch(() => null))) { - console.error('🚨 keystone build must be run before running keystone prisma') - throw new ExitError(1) +async function spawnPrisma3 (cwd: string, system: { + config: { + db: { + url: string + } } - - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig - const config = getBuiltKeystoneConfiguration(cwd) - const system = createSystem(config) - - await validateArtifacts(cwd, system) - console.log('✨ GraphQL and Prisma schemas are up to date') - - return new Promise((resolve, reject) => { - const p = spawn('node', [require.resolve('prisma'), ...args], { +}, commands: string[]) { + return new Promise<{ + exitCode: number | null + }>((resolve, reject) => { + const p = spawn('node', [require.resolve('prisma'), ...commands], { cwd, env: { ...process.env, DATABASE_URL: system.config.db.url, PRISMA_HIDE_UPDATE_MESSAGE: '1', }, - stdio: 'inherit', + stdio: 'inherit' }) p.on('error', err => reject(err)) - p.on('exit', code => { - if (code) return reject(new ExitError(Number(code))) - resolve() - }) + p.on('exit', exitCode => (resolve({ exitCode }))) }) } + +export async function prisma (cwd: string, args: string[], frozen: boolean) { + // TODO: should build unless --frozen? + + const system = createSystem(await importBuiltKeystoneConfiguration(cwd)) + + await validateArtifacts(cwd, system) + console.log('✨ GraphQL and Prisma schemas are up to date') + + const { exitCode } = await spawnPrisma3(cwd, system, args) + if (typeof exitCode === 'number' && exitCode !== 0) { + throw new ExitError(exitCode) + } +} diff --git a/packages/core/src/scripts/start.ts b/packages/core/src/scripts/start.ts index d983cc67c04..0c534f59afe 100644 --- a/packages/core/src/scripts/start.ts +++ b/packages/core/src/scripts/start.ts @@ -1,14 +1,10 @@ -import fs from 'node:fs/promises' import next from 'next' -import { - createSystem, - getBuiltKeystoneConfigurationPath, - getBuiltKeystoneConfiguration, -} from '../lib/createSystem' + +import { createSystem } from '../lib/createSystem' import { createExpressServer } from '../lib/createExpressServer' import { createAdminUIMiddlewareWithNextApp } from '../lib/createAdminUIMiddleware' -import { runMigrationsOnDatabase } from '../lib/migrations' -import { ExitError } from './utils' +import { withMigrate } from '../lib/migrations' +import { importBuiltKeystoneConfiguration } from './utils' import { type Flags } from './cli' export async function start ( @@ -17,22 +13,13 @@ export async function start ( ) { console.log('✨ Starting Keystone') - // TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig - const builtConfigPath = getBuiltKeystoneConfigurationPath(cwd) - - // this is the compiled version of the configuration which was generated during the build step - if (!(await fs.stat(builtConfigPath).catch(() => null))) { - console.error('🚨 keystone build must be run before running keystone start') - throw new ExitError(1) - } - - const system = createSystem(getBuiltKeystoneConfiguration(cwd)) + const system = createSystem(await importBuiltKeystoneConfiguration(cwd)) const paths = system.getPaths(cwd) if (withMigrations) { console.log('✨ Applying any database migrations') - const migrations = await runMigrationsOnDatabase(cwd, system) - console.log(migrations.length === 0 ? `✨ No database migrations to apply` : `✨ Database migrated`) + const { appliedMigrationNames } = await withMigrate(paths.schema.prisma, system, (m) => m.apply()) + console.log(appliedMigrationNames.length === 0 ? `✨ No database migrations to apply` : `✨ Database migrated`) } if (!server) return diff --git a/packages/core/src/scripts/utils.ts b/packages/core/src/scripts/utils.ts index 42b8c2d27fc..840c6c420c2 100644 --- a/packages/core/src/scripts/utils.ts +++ b/packages/core/src/scripts/utils.ts @@ -1,7 +1,19 @@ +import { getBuiltKeystoneConfigurationPath } from '../lib/createSystem' + export class ExitError extends Error { code: number constructor (code: number) { - super(`The process should exit with ${code}`) + super(`The process exited with Error ${code}`) this.code = code } } + +// TODO: this cannot be changed for now, circular dependency with getSystemPaths, getEsbuildConfig +export async function importBuiltKeystoneConfiguration (cwd: string) { + try { + return require(getBuiltKeystoneConfigurationPath(cwd)).default + } catch (e) { + console.error('🚨 keystone build has not been run') + throw new ExitError(1) + } +} diff --git a/packages/core/src/testing.ts b/packages/core/src/testing.ts index 2624ca2f808..c66c99438c1 100644 --- a/packages/core/src/testing.ts +++ b/packages/core/src/testing.ts @@ -1 +1,19 @@ -export { resetDatabase } from './lib/migrations' +import path from 'node:path' +import { createDatabase, } from '@prisma/internals' + +import { withMigrate } from './lib/migrations' + +export async function resetDatabase (dbUrl: string, prismaSchemaPath: string) { + await createDatabase(dbUrl, path.dirname(prismaSchemaPath)) + await withMigrate(prismaSchemaPath, { + config: { + db: { + url: dbUrl, + shadowDatabaseUrl: '' + } + }, + }, async (m) => { + await m.reset() + await m.push(true) + }) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aca06e5f169..7db6e72f00e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1971,14 +1971,14 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@prisma/client': - specifier: 5.14.0 - version: 5.14.0(prisma@5.14.0) + specifier: 5.15.1 + version: 5.15.1(prisma@5.15.1) '@prisma/internals': - specifier: 5.14.0 - version: 5.14.0 + specifier: 5.15.1 + version: 5.15.1 '@prisma/migrate': - specifier: 5.14.0 - version: 5.14.0(@prisma/generator-helper@5.14.0)(@prisma/internals@5.14.0) + specifier: 5.15.1 + version: 5.15.1(@prisma/generator-helper@5.15.1)(@prisma/internals@5.15.1) '@sindresorhus/slugify': specifier: ^1.1.2 version: 1.1.2 @@ -2064,8 +2064,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 prisma: - specifier: 5.14.0 - version: 5.14.0 + specifier: 5.15.1 + version: 5.15.1 prompts: specifier: ^2.4.2 version: 2.4.2 @@ -2433,7 +2433,7 @@ importers: version: 5.14.0 '@prisma/migrate': specifier: ^5.0.0 - version: 5.14.0(@prisma/generator-helper@5.14.0)(@prisma/internals@5.14.0) + version: 5.14.0(@prisma/generator-helper@5.15.1)(@prisma/internals@5.14.0) chalk: specifier: ^4.1.2 version: 4.1.2 @@ -4921,42 +4921,84 @@ packages: prisma: optional: true + '@prisma/client@5.15.1': + resolution: {integrity: sha512-fmZRGmsUJ9+VwC/AvfP/PwdpD0xAEyPvNsD9/B3+GYpETq9VejVRT3PiqNvl76q1uYYzNZeo8u/LmzzTetHSEg==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + '@prisma/debug@5.14.0': resolution: {integrity: sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==} + '@prisma/debug@5.15.1': + resolution: {integrity: sha512-NQjdEplhXEcPvf84ghxExC+LD+iTimbg3sZvA3BhybVQIocBEBxFf9GTHhmRVPmjrWoBaYJBVgEEBXZT27JTbQ==} + '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==} + '@prisma/engines-version@5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3': + resolution: {integrity: sha512-7csphKGCG6n/cN1MkT1mJvQ78Ir18IknlYZ8eyEoLKdQBb0HscR/6TyPmzqrMA7Rz01K1KeXqctwAqxtA/lKQg==} + '@prisma/engines@5.14.0': resolution: {integrity: sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==} + '@prisma/engines@5.15.1': + resolution: {integrity: sha512-1iTRxJEFvpBpEWf2bYiMG6LBBQhX7X+GA5piH+tmPWgc/v+/ElxQf2kjQxby8AErmZqtZkdoKJ7FSRjNjBPE9Q==} + '@prisma/fetch-engine@5.14.0': resolution: {integrity: sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==} + '@prisma/fetch-engine@5.15.1': + resolution: {integrity: sha512-mj0wfsJ+mAdDp1ynT2JKxAXa+CoYMT267qF7g2Uv+oaVTI2CMfGWouMARht8T2QLTgl+gpXSFTwIYbcR+oWEtw==} + '@prisma/generator-helper@5.14.0': resolution: {integrity: sha512-xVc71cmTnPZ0lnSs4FAY6Ta72vFJ3webrQwKMQ2ujr6hDG1VPIEf820T1TOS3ZZQd/OKigNKXnq3co8biz9/qw==} + '@prisma/generator-helper@5.15.1': + resolution: {integrity: sha512-eTYGy6FSk2qflQfoXyzGhyCVIIAf+yL7YoSfLDBExSOB9kgug7pfSEWSwkce0CRxUskzx0XU/I4GDBj02laMfg==} + '@prisma/get-platform@5.14.0': resolution: {integrity: sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==} + '@prisma/get-platform@5.15.1': + resolution: {integrity: sha512-oFccp7bYys+ZYkmtYzjR+0cRrGKvSuF+h5QhSkyEsYQ9kzJzQRvuWt2SiHRPt8xOQ4MTmujM+bP5uOexnnAHdQ==} + '@prisma/internals@5.14.0': resolution: {integrity: sha512-s0JRNDmR2bvcyy0toz89jy7SbbjANAs4e9KCReNvSm5czctIaZzDf68tcOXdtH0G7m9mKhVhNPdS9lMky0DhWA==} + '@prisma/internals@5.15.1': + resolution: {integrity: sha512-kXoKRbt7g+65347aS5c082UJ4mNvYYrwL/MkFGOs+W7Rvauh4JhNuzIyv4R+04ETdojsTi0yL9ZD7lKR9DxXYQ==} + '@prisma/migrate@5.14.0': resolution: {integrity: sha512-/1sZWxtQojUS1UoyuElaKviqwkUAMjzQ2IKoXP1RHFhxb7bzq+WKcqj9c98ACq/aRST9YJi/j3YQjwLa5ciAiQ==} peerDependencies: '@prisma/generator-helper': '*' '@prisma/internals': '*' + '@prisma/migrate@5.15.1': + resolution: {integrity: sha512-9HVlrIgTBL6ttKBvKsBS+N2wkL5nklo5MMD2D6dbB3MpSyrwPtjKwNXIBEZYZMLifOm2jZW6VvhGwA2lXIAM+w==} + peerDependencies: + '@prisma/generator-helper': '*' + '@prisma/internals': '*' + '@prisma/prisma-schema-wasm@5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85': resolution: {integrity: sha512-SX9vE9dGYBap6xsfJuDE5b2eoA6w1vKsx8QpLUHZR+kIV6GQVUYUboEfkvYYoBVen3s9LqxJ1+LjHL/1MqBZag==} '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': resolution: {integrity: sha512-WeTmJ0mK8ALoKJUQFO+465k9lm1JWS4ODUg7akJq1wjgyDU1RTAzDFli8ESmNJlMVgJgoAd6jXmzcnoA0HT9Lg==} + '@prisma/prisma-schema-wasm@5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3': + resolution: {integrity: sha512-7oN1RVcb7PA4M2vJbxYUlkU7wDcUPPOE977JvfD1BJTxmEmfSzoTM6Z0BNQN+Ukl7CHOOPiko3tdM1CzxOEOfQ==} + '@prisma/schema-files-loader@5.14.0': resolution: {integrity: sha512-n1QHR2C63dARKPZe0WPn7biybcBHzXe+BEmiHC5Drq9KPWnpmQtIfGpqm1ZKdvCZfcA5FF3wgpSMPK4LnB0obQ==} + '@prisma/schema-files-loader@5.15.1': + resolution: {integrity: sha512-KmqciQNkmhuv5xRKzfjre0D/sB2JKbQQfdk8qnMI/mEw1bHTzME0n7nJrElx/ivx7Ndai8ejgFTnlGxcxS+vdA==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -9989,6 +10031,11 @@ packages: engines: {node: '>=16.13'} hasBin: true + prisma@5.15.1: + resolution: {integrity: sha512-pYsUVpTlYvZ6mWvZKDv9rKdUa7tlfSUJY1CVtgb8Had1pHbIm9fr1MBASccr5XnSuCUrjnvKhWNwgSYy6aCajA==} + engines: {node: '>=16.13'} + hasBin: true + prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} @@ -14856,10 +14903,18 @@ snapshots: optionalDependencies: prisma: 5.14.0 + '@prisma/client@5.15.1(prisma@5.15.1)': + optionalDependencies: + prisma: 5.15.1 + '@prisma/debug@5.14.0': {} + '@prisma/debug@5.15.1': {} + '@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} + '@prisma/engines-version@5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3': {} + '@prisma/engines@5.14.0': dependencies: '@prisma/debug': 5.14.0 @@ -14867,20 +14922,41 @@ snapshots: '@prisma/fetch-engine': 5.14.0 '@prisma/get-platform': 5.14.0 + '@prisma/engines@5.15.1': + dependencies: + '@prisma/debug': 5.15.1 + '@prisma/engines-version': 5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3 + '@prisma/fetch-engine': 5.15.1 + '@prisma/get-platform': 5.15.1 + '@prisma/fetch-engine@5.14.0': dependencies: '@prisma/debug': 5.14.0 '@prisma/engines-version': 5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48 '@prisma/get-platform': 5.14.0 + '@prisma/fetch-engine@5.15.1': + dependencies: + '@prisma/debug': 5.15.1 + '@prisma/engines-version': 5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3 + '@prisma/get-platform': 5.15.1 + '@prisma/generator-helper@5.14.0': dependencies: '@prisma/debug': 5.14.0 + '@prisma/generator-helper@5.15.1': + dependencies: + '@prisma/debug': 5.15.1 + '@prisma/get-platform@5.14.0': dependencies: '@prisma/debug': 5.14.0 + '@prisma/get-platform@5.15.1': + dependencies: + '@prisma/debug': 5.15.1 + '@prisma/internals@5.14.0': dependencies: '@prisma/debug': 5.14.0 @@ -14893,24 +14969,52 @@ snapshots: arg: 5.0.2 prompts: 2.4.2 - '@prisma/migrate@5.14.0(@prisma/generator-helper@5.14.0)(@prisma/internals@5.14.0)': + '@prisma/internals@5.15.1': + dependencies: + '@prisma/debug': 5.15.1 + '@prisma/engines': 5.15.1 + '@prisma/fetch-engine': 5.15.1 + '@prisma/generator-helper': 5.15.1 + '@prisma/get-platform': 5.15.1 + '@prisma/prisma-schema-wasm': 5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3 + '@prisma/schema-files-loader': 5.15.1 + arg: 5.0.2 + prompts: 2.4.2 + + '@prisma/migrate@5.14.0(@prisma/generator-helper@5.15.1)(@prisma/internals@5.14.0)': dependencies: '@prisma/debug': 5.14.0 '@prisma/engines-version': 5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48 - '@prisma/generator-helper': 5.14.0 + '@prisma/generator-helper': 5.15.1 '@prisma/get-platform': 5.14.0 '@prisma/internals': 5.14.0 prompts: 2.4.2 + '@prisma/migrate@5.15.1(@prisma/generator-helper@5.15.1)(@prisma/internals@5.15.1)': + dependencies: + '@prisma/debug': 5.15.1 + '@prisma/engines-version': 5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3 + '@prisma/generator-helper': 5.15.1 + '@prisma/get-platform': 5.15.1 + '@prisma/internals': 5.15.1 + prompts: 2.4.2 + '@prisma/prisma-schema-wasm@5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85': {} '@prisma/prisma-schema-wasm@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48': {} + '@prisma/prisma-schema-wasm@5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3': {} + '@prisma/schema-files-loader@5.14.0': dependencies: '@prisma/prisma-schema-wasm': 5.14.0-17.56ca112d5a19c9925b53af75c3c6b7ada97f9f85 fs-extra: 11.1.1 + '@prisma/schema-files-loader@5.15.1': + dependencies: + '@prisma/prisma-schema-wasm': 5.15.1-1.5675a3182f972f1a8f31d16eee6abf4fd54910e3 + fs-extra: 11.1.1 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -21398,6 +21502,10 @@ snapshots: dependencies: '@prisma/engines': 5.14.0 + prisma@5.15.1: + dependencies: + '@prisma/engines': 5.15.1 + prismjs@1.29.0: {} private@0.1.8: {} diff --git a/tests/api-tests/queries/filters.test.ts b/tests/api-tests/queries/filters.test.ts index 42acc88f321..b3c23703b2c 100644 --- a/tests/api-tests/queries/filters.test.ts +++ b/tests/api-tests/queries/filters.test.ts @@ -3,11 +3,11 @@ import { list } from '@keystone-6/core' import { setupTestRunner } from '@keystone-6/api-tests/test-runner' import { allowAll } from '@keystone-6/core/access' import { + type ContextFromRunner, expectAccessReturnError, expectBadUserInput, expectGraphQLValidationError, expectFilterDenied, - type ContextFromRunner } from '../utils' const runner = setupTestRunner({ @@ -78,7 +78,7 @@ const runner = setupTestRunner({ }, }) -const initialiseData = async ({ context }: { context: ContextFromRunner }) => { +async function initialiseData ({ context }: { context: ContextFromRunner }) { // Use shuffled data to ensure that ordering is actually happening. for (const listKey of Object.keys(context.query) as Array) { if (listKey === 'User' || listKey === 'SecondaryList') continue diff --git a/tests/api-tests/test-runner.ts b/tests/api-tests/test-runner.ts index d901615518f..551ac04880f 100644 --- a/tests/api-tests/test-runner.ts +++ b/tests/api-tests/test-runner.ts @@ -4,8 +4,10 @@ import { join } from 'node:path' import { randomBytes } from 'node:crypto' import { readdirSync } from 'node:fs' import { tmpdir } from 'node:os' + import supertest from 'supertest' import { + createDatabase, getConfig, getDMMF, parseEnvValue, @@ -19,7 +21,7 @@ import { createExpressServer, createSystem, generateArtifacts, - pushPrismaSchemaToDatabase + withMigrate } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/artifacts' import { @@ -135,9 +137,14 @@ export async function setupTestEnv ({ }) const artifacts = await generateArtifacts(cwd, system) - await pushPrismaSchemaToDatabase(cwd, system, artifacts.prisma) - const paths = system.getPaths(cwd) + + await createDatabase(system.config.db.url, cwd) + await withMigrate(paths.schema.prisma, system, async (m) => { + await m.reset() + await m.schema(artifacts.prisma, false) + }) + const { context, connect, diff --git a/tests/cli-tests/artifacts.test.ts b/tests/cli-tests/artifacts.test.ts index abceb042754..9af15bc64a7 100644 --- a/tests/cli-tests/artifacts.test.ts +++ b/tests/cli-tests/artifacts.test.ts @@ -1,9 +1,10 @@ +import { ExitError } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/artifacts' + import { - ExitError, basicKeystoneConfig, getFiles, recordConsole, - runCommand, + cliMock, schemas, symlinkKeystoneDeps, testdir, @@ -11,13 +12,13 @@ import { describe.each(['postinstall', ['build', '--frozen']])('%s', command => { test('logs an error and exits with 1 when the schemas do not exist', async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, 'keystone.js': basicKeystoneConfig, }) const recording = recordConsole() - await expect(runCommand(tmp, command)).rejects.toEqual(new ExitError(1)) + await expect(cliMock(cwd, command)).rejects.toEqual(new ExitError(1)) expect(recording()).toMatchInlineSnapshot(`"Your Prisma and GraphQL schemas are not up to date"`) }) @@ -25,14 +26,14 @@ describe.each(['postinstall', ['build', '--frozen']])('%s', command => { describe('prisma migrate status', () => { test('logs an error and exits with 1 when the schemas do not exist', async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, 'keystone.js': basicKeystoneConfig, }) - await expect(runCommand(tmp, ['build', '--no-ui', '--frozen'])).rejects.toEqual(new ExitError(1)) + await expect(cliMock(cwd, ['build', '--no-ui', '--frozen'])).rejects.toEqual(new ExitError(1)) const recording = recordConsole() - await expect(runCommand(tmp, ['prisma', '--frozen', 'migrate', 'status'])).rejects.toEqual(new ExitError(1)) + await expect(cliMock(cwd, ['prisma', '--frozen', 'migrate', 'status'])).rejects.toEqual(new ExitError(1)) expect(recording()).toMatchInlineSnapshot(`"Your Prisma and GraphQL schemas are not up to date"`) }) @@ -45,45 +46,45 @@ const schemasMatch = ['schema.prisma', 'schema.graphql'] // (and in the case of the build command we need to spawn a child process which would make each case take a _very_ long time) describe('postinstall', () => { test('updates the schemas without prompting when --fix is passed', async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, 'keystone.js': basicKeystoneConfig, }) const recording = recordConsole() - await runCommand(tmp, ['postinstall', '--fix']) - const files = await getFiles(tmp, schemasMatch) + await cliMock(cwd, ['postinstall', '--fix']) + const files = await getFiles(cwd, schemasMatch) expect(files).toEqual(await getFiles(`${__dirname}/fixtures/basic-project`, schemasMatch)) expect(recording()).toMatchInlineSnapshot(`"? Generated GraphQL and Prisma schemas"`) }) test("does not prompt, error or modify the schemas if they're already up to date", async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, ...schemas, 'keystone.js': basicKeystoneConfig, }) const recording = recordConsole() - await runCommand(tmp, 'postinstall') - const files = await getFiles(tmp, schemasMatch) + await cliMock(cwd, 'postinstall') + const files = await getFiles(cwd, schemasMatch) expect(files).toEqual(await getFiles(`${__dirname}/fixtures/basic-project`, schemasMatch)) expect(recording()).toMatchInlineSnapshot(`"? GraphQL and Prisma schemas are up to date"`) }) test('writes the correct node_modules files', async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, ...schemas, 'keystone.js': basicKeystoneConfig, }) const recording = recordConsole() - await runCommand(tmp, 'postinstall') + await cliMock(cwd, 'postinstall') - expect(await getFiles(tmp, ['node_modules/.keystone/**/*'])).toMatchSnapshot() + expect(await getFiles(cwd, ['node_modules/.keystone/**/*'])).toMatchSnapshot() expect(recording()).toMatchInlineSnapshot(`"? GraphQL and Prisma schemas are up to date"`) }) }) diff --git a/tests/cli-tests/build.test.ts b/tests/cli-tests/build.test.ts index faec50e5d31..3089758367f 100644 --- a/tests/cli-tests/build.test.ts +++ b/tests/cli-tests/build.test.ts @@ -1,24 +1,25 @@ import fs from 'node:fs/promises' import execa from 'execa' import { - ExitError, basicKeystoneConfig, cliBinPath, recordConsole, - runCommand, + cliMock, schemas, symlinkKeystoneDeps, testdir, } from './utils' +import { ExitError } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/artifacts' + test("start errors when a build hasn't happened", async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, ...schemas, 'keystone.js': basicKeystoneConfig, }) const recording = recordConsole() - await expect(runCommand(tmp, 'start')).rejects.toEqual(new ExitError(1)) + await expect(cliMock(cwd, 'start')).rejects.toEqual(new ExitError(1)) expect(recording()).toMatchInlineSnapshot(` "? Starting Keystone ? keystone build must be run before running keystone start" @@ -28,7 +29,7 @@ test("start errors when a build hasn't happened", async () => { jest.setTimeout(1000000) test('build works with typescript without the user defining a babel config', async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, ...schemas, 'keystone.ts': await fs.readFile(`${__dirname}/fixtures/with-ts.ts`, 'utf8'), @@ -36,7 +37,7 @@ test('build works with typescript without the user defining a babel config', asy const result = await execa('node', [cliBinPath, 'build'], { reject: false, all: true, - cwd: tmp, + cwd, env: { NEXT_TELEMETRY_DISABLED: '1', } as any, @@ -48,7 +49,7 @@ test('build works with typescript without the user defining a babel config', asy }) test('process.env.NODE_ENV is production in production', async () => { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, ...schemas, 'keystone.ts': await fs.readFile(`${__dirname}/fixtures/log-node-env.ts`, 'utf8'), @@ -56,7 +57,7 @@ test('process.env.NODE_ENV is production in production', async () => { const result = await execa('node', [cliBinPath, 'build'], { reject: false, all: true, - cwd: tmp, + cwd, buffer: true, env: { NEXT_TELEMETRY_DISABLED: '1', @@ -66,7 +67,7 @@ test('process.env.NODE_ENV is production in production', async () => { const startResult = execa('node', [cliBinPath, 'start'], { reject: false, all: true, - cwd: tmp, + cwd, env: { NODE_ENV: 'production', NEXT_TELEMETRY_DISABLED: '1', diff --git a/tests/cli-tests/migrations.test.ts b/tests/cli-tests/migrations.test.ts index 7a55cf31b1a..e6f4a4f03d2 100644 --- a/tests/cli-tests/migrations.test.ts +++ b/tests/cli-tests/migrations.test.ts @@ -1,12 +1,11 @@ import path from 'node:path' import fs from 'node:fs' import fsp from 'node:fs/promises' -import { ExitError } from './utils' import { getFiles, - introspectDb, + introspectDatabase, recordConsole, - runCommand, + cliMock, spawnCommand, symlinkKeystoneDeps, testdir, @@ -17,31 +16,49 @@ const config0 = fs.readFileSync(`${__dirname}/fixtures/no-fields.ts`, 'utf8') const config1 = fs.readFileSync(`${__dirname}/fixtures/one-field.ts`, 'utf8') const config2 = fs.readFileSync(`${__dirname}/fixtures/two-fields.ts`, 'utf8') -const schema1 = `datasource db { - provider = "sqlite" - url = "file:./app.db" +const schema1 = `generator client { + provider = "prisma-client-js" + output = "node_modules/.testprisma/client" +} + +datasource sqlite { + provider = "sqlite" + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } model Todo { - id String @id + id String @id @default(cuid()) title String @default("") } + + ` -const schema2 = `datasource db { - provider = "sqlite" - url = "file:./app.db" +const schema2 = `generator client { + provider = "prisma-client-js" + output = "node_modules/.testprisma/client" +} + +datasource sqlite { + provider = "sqlite" + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") } model Todo { - id String @id + id String @id @default(cuid()) title String @default("") isComplete Boolean @default(false) } + + ` let mockPromptResponseEntries: [string, string | boolean][] = [] +jest.setTimeout(60 * 1000) // these tests are slow + jest.mock('prompts', () => { return function ( args: @@ -93,29 +110,28 @@ function getPrismaClient (cwd: string) { }) } -// TODO: when we can make fields non-nullable, we should have tests for unexecutable migrations describe('dev', () => { async function setupInitialProject () { - const tmp = await testdir({ + const cwd = await testdir({ ...symlinkKeystoneDeps, 'keystone.js': config1, }) - const recording = recordConsole() + const stopRecording = recordConsole() - await runCommand(tmp, 'dev') - expect(await introspectDb(tmp, dbUrl)).toEqual(schema1) - expect(recording()).toMatchInlineSnapshot(` + await cliMock(cwd, 'dev') + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) + expect(stopRecording()).toMatchInlineSnapshot(` "? Starting Keystone ? Server listening on :3000 (http://localhost:3000/) ? GraphQL API available at /api/graphql ? Generating GraphQL and Prisma schemas - ? sqlite database "app.db" created at file:./app.db + ? database created ? Database synchronized with Prisma schema ? Connecting to the database ? Creating server ? GraphQL API ready" `) - return tmp + return cwd } test('creates database and pushes schema from empty', async () => { @@ -123,11 +139,11 @@ describe('dev', () => { }) test('logs correctly when things are already up to date', async () => { - const tmp = await setupInitialProject() - const recording = recordConsole() - await runCommand(tmp, 'dev') + const cwd = await setupInitialProject() + const stopRecording = recordConsole() + await cliMock(cwd, 'dev') - expect(recording()).toMatchInlineSnapshot(` + expect(stopRecording()).toMatchInlineSnapshot(` "? Starting Keystone ? Server listening on :3000 (http://localhost:3000/) ? GraphQL API available at /api/graphql @@ -144,27 +160,18 @@ describe('dev', () => { const prismaClient = getPrismaClient(prevCwd) await prismaClient.todo.create({ data: { title: 'todo' } }) await prismaClient.$disconnect() + const cwd = await testdir({ ...symlinkKeystoneDeps, ...(await getDatabaseFiles(prevCwd)), 'keystone.js': config0, }) - mockPromptResponseEntries = [['Do you want to continue? Some data will be lost', true]] - const recording = recordConsole() - await runCommand(cwd, 'dev') - expect(await introspectDb(cwd, dbUrl)).toMatchInlineSnapshot(` - "datasource db { - provider = "sqlite" - url = "file:./app.db" - } + mockPromptResponseEntries = [['Do you want to continue? Some data will be lost', true]] + const stopRecording = recordConsole() + await cliMock(cwd, 'dev') - model Todo { - id String @id - } - " - `) - expect(recording()).toMatchInlineSnapshot(` + expect(stopRecording()).toMatchInlineSnapshot(` "? Starting Keystone ? Server listening on :3000 (http://localhost:3000/) ? GraphQL API available at /api/graphql @@ -179,6 +186,26 @@ describe('dev', () => { ? Creating server ? GraphQL API ready" `) + + expect(await introspectDatabase(cwd, dbUrl)).toMatchInlineSnapshot(` + "generator client { + provider = "prisma-client-js" + output = "node_modules/.testprisma/client" + } + + datasource sqlite { + provider = "sqlite" + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + } + + model Todo { + id String @id @default(cuid()) + } + + + " + `) }) test('exits when refusing data loss prompt', async () => { @@ -193,22 +220,11 @@ describe('dev', () => { }) mockPromptResponseEntries = [['Do you want to continue? Some data will be lost', false]] - const recording = recordConsole() - await expect(runCommand(cwd, 'dev')).rejects.toEqual(new ExitError(0)) - - expect(await introspectDb(cwd, dbUrl)).toMatchInlineSnapshot(` - "datasource db { - provider = "sqlite" - url = "file:./app.db" - } + const stopRecording = recordConsole() + await expect(cliMock(cwd, 'dev')).rejects.toEqual(expect.objectContaining({ code: 0 })) - model Todo { - id String @id - title String @default("") - } - " - `) - expect(recording()).toMatchInlineSnapshot(` + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) + expect(stopRecording()).toMatchInlineSnapshot(` "? Starting Keystone ? Server listening on :3000 (http://localhost:3000/) ? GraphQL API available at /api/graphql @@ -239,6 +255,7 @@ describe('prisma', () => { { const prismaClient = getPrismaClient(cwd) await prismaClient.todo.create({ data: { title: 'something' } }) + expect(await prismaClient.todo.findMany()).toHaveLength(1) await prismaClient.$disconnect() } @@ -269,10 +286,10 @@ describe('start --with-migrations', () => { await spawnCommand(cwd, ['build', '--no-ui']) await spawnCommand(cwd, ['prisma', 'migrate', 'dev', '--name', 'init', '--create-only']) - expect(await introspectDb(cwd, dbUrl)).toEqual(null) // empty + expect(await introspectDatabase(cwd, dbUrl)).toEqual('') // empty const output = await spawnCommand(cwd, ['start', '--no-server', '--no-ui', '--with-migrations']) - expect(await introspectDb(cwd, dbUrl)).toEqual(schema1) // migrated + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) // migrated expect( output @@ -298,16 +315,16 @@ describe('start --with-migrations', () => { await spawnCommand(cwd, ['build', '--no-ui']) await spawnCommand(cwd, ['prisma', 'migrate', 'dev', '--name', 'init', '--create-only']) await spawnCommand(cwd, ['prisma', 'migrate', 'deploy']) - expect(await introspectDb(cwd, dbUrl)).toEqual(schema1) + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) // step 2, add a field await fsp.writeFile(`${cwd}/keystone.ts`, config2) await spawnCommand(cwd, ['build', '--no-ui']) await spawnCommand(cwd, ['prisma', 'migrate', 'dev', '--name', 'add', '--create-only']) - expect(await introspectDb(cwd, dbUrl)).toEqual(schema1) // unchanged + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) // unchanged const output = await spawnCommand(cwd, ['start', '--no-server', '--no-ui', '--with-migrations']) - expect(await introspectDb(cwd, dbUrl)).toEqual(schema2) // migrated + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema2) // migrated expect( output @@ -332,9 +349,9 @@ describe('start --with-migrations', () => { await spawnCommand(cwd, ['prisma', 'migrate', 'dev', '--name', 'init', '--create-only']) await spawnCommand(cwd, ['prisma', 'migrate', 'deploy']) - expect(await introspectDb(cwd, dbUrl)).toEqual(schema1) // unchanged + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) // unchanged const output = await spawnCommand(cwd, ['start', '--no-server', '--no-ui', '--with-migrations']) - expect(await introspectDb(cwd, dbUrl)).toEqual(schema1) // unchanged + expect(await introspectDatabase(cwd, dbUrl)).toEqual(schema1) // unchanged expect(output.replace(/[^ -~\n]+/g, '?')).toMatchInlineSnapshot(` "? Starting Keystone diff --git a/tests/cli-tests/utils.ts b/tests/cli-tests/utils.ts index 25b21eadc72..9be8707403a 100644 --- a/tests/cli-tests/utils.ts +++ b/tests/cli-tests/utils.ts @@ -7,22 +7,11 @@ import * as fse from 'fs-extra' import fastGlob from 'fast-glob' import chalk from 'chalk' -import { SchemaEngine } from '@prisma/migrate' -import { uriToCredentials } from '@prisma/internals' import { cli } from '@keystone-6/core/scripts/cli' // these tests spawn processes and it's all pretty slow jest.setTimeout(1000 * 20) -// some of these utilities come from https://github.com/preconstruct/preconstruct/blob/07a24f73f17980c121382bb00ae1c05355294fe4/packages/cli/test-utils/index.ts -export class ExitError extends Error { - code: number - constructor (code: number) { - super(`The process should exit with ${code}`) - this.code = code - } -} - export const cliBinPath = require.resolve('@keystone-6/core/bin/cli.js') export const basicKeystoneConfig = fs.readFileSync( @@ -74,7 +63,7 @@ type Fixture = { | { kind: 'symlink', path: string } } -export async function runCommand (cwd: string, args: string | string[]) { +export async function cliMock (cwd: string, args: string | string[]) { const argv = typeof args === 'string' ? [args] : args chalk.level = 0 // disable ANSI colouring for this const proc = await cli(cwd, argv) @@ -91,7 +80,7 @@ export async function spawnCommand (cwd: string, commands: string[]) { p.stderr.on('data', (data) => (output += data.toString('utf-8'))) p.on('error', err => reject(err)) p.on('exit', exitCode => { - if (typeof exitCode === 'number' && exitCode !== 0) return reject(new ExitError(exitCode)) + if (typeof exitCode === 'number' && exitCode !== 0) return reject(`${commands.join(' ')} returned ${exitCode}`) resolve(output) }) }) @@ -122,6 +111,7 @@ afterAll(async () => { dirsToRemove = [] }) +// from https://github.com/preconstruct/preconstruct/blob/07a24f73f17980c121382bb00ae1c05355294fe4/packages/cli/test-utils/index.ts export async function testdir (dir: Fixture) { const temp = await fsp.mkdtemp(__dirname) dirsToRemove.push(temp) @@ -144,6 +134,7 @@ export async function testdir (dir: Fixture) { return temp } +// from https://github.com/preconstruct/preconstruct/blob/07a24f73f17980c121382bb00ae1c05355294fe4/packages/cli/test-utils/index.ts expect.addSnapshotSerializer({ print (_val) { const val = _val as Record @@ -167,42 +158,42 @@ expect.addSnapshotSerializer({ const dirPrintingSymbol = Symbol('dir printing symbol') +// derived from https://github.com/preconstruct/preconstruct/blob/07a24f73f17980c121382bb00ae1c05355294fe4/packages/cli/test-utils/index.ts export async function getFiles ( dir: string, glob: string[] = ['**', '!node_modules/**'], encoding: 'utf8' | null = 'utf8' ) { const files = await fastGlob(glob, { cwd: dir }) - const filesObj: Record = { + const result: Record = { [dirPrintingSymbol]: true, } await Promise.all( - files.map(async filename => { - filesObj[filename] = await fsp.readFile(path.join(dir, filename), encoding) + files.sort().map(async (fileName: string) => { + result[fileName] = await fsp.readFile(path.join(dir, fileName), encoding) }) ) - const result: Record = { [dirPrintingSymbol]: true } - files.sort().forEach(filename => { - result[filename] = filesObj[filename] - }) return result } -export async function introspectDb (cwd: string, url: string) { - const engine = new SchemaEngine({ projectDir: cwd }) - try { - const { datamodel } = await engine.introspect({ - schema: `datasource db { - url = ${JSON.stringify(url)} - provider = ${JSON.stringify(uriToCredentials(url).type)} -}`, +export async function introspectDatabase (cwd: string, url: string) { + let output = '' + return new Promise((resolve, reject) => { + const p = spawn('node', [require.resolve('prisma'), 'db', 'pull', '--print'], { + cwd, + env: { + ...process.env, + DATABASE_URL: url, + PRISMA_HIDE_UPDATE_MESSAGE: '1', + }, }) - return datamodel - } catch (e: any) { - if (e.code === 'P4001') return null - throw e - - } finally { - engine.stop() - } + p.stdout.on('data', (data) => (output += data.toString('utf-8'))) + p.stderr.on('data', (data) => (output += data.toString('utf-8'))) + p.on('error', err => reject(err)) + p.on('exit', exitCode => { + if (output.includes('P4001')) return resolve('') // empty database + if (typeof exitCode === 'number' && exitCode !== 0) return reject(`Introspect process returned ${exitCode}`) + resolve(output) + }) + }) }