import { getSchemaDirSync } from '@prisma/cli'
import { getGenerators, ProviderAliases } from '@prisma/sdk'
import chalk from 'chalk'
import { spawn } from 'child_process'
import cliCursor from 'cli-cursor'
import dashify from 'dashify'
import debugLib from 'debug'
import fs from 'fs'
import getPort from 'get-port'
import globby from 'globby'
import indent from 'indent-string'
import logUpdate from 'log-update'
import makeDir = require('make-dir')
import pMap from 'p-map'
import path from 'path'
import { prompt } from 'prompts'
import rimraf from 'rimraf'
import { Readable } from 'stream'
import stripAnsi from 'strip-ansi'
import { promisify } from 'util'
import { highlightDatamodel } from './cli/highlight/highlight'
import { blue } from './cli/highlight/theme'
import { DevComponentRenderer } from './ink/DevComponentRenderer'
import { LiftEngine } from './LiftEngine'
import { Studio } from './Studio'
import { EngineResults, FileMap, LocalMigration, LocalMigrationWithDatabaseSteps, LockFile, Migration } from './types'
import { drawBox } from './utils/drawBox'
import { exit } from './utils/exit'
import { formatms } from './utils/formartms'
import { getDatamodelPath } from './utils/getDatamodelPath'
import { groupBy } from './utils/groupBy'
import { isWatchMigrationName } from './utils/isWatchMigrationName'
import { deserializeLockFile, initLockFile, serializeLockFile } from './utils/LockFile'
import { maskSchema } from './utils/maskSchema'
import { now, timestampToDate } from './utils/now'
import plusX from './utils/plusX'
import { highlightMigrationsSQL, printDatabaseStepsOverview } from './utils/printDatabaseSteps'
import { printDatamodelDiff } from './utils/printDatamodelDiff'
import { printMigrationReadme } from './utils/printMigrationReadme'
import { serializeFileMap } from './utils/serializeFileMap'
import { simpleDebounce } from './utils/simpleDebounce'
import { flatMap } from './utils/flatMap'
const debug = debugLib('Lift')
const packageJson = eval(`require('../package.json')`) // tslint:disable-line

const del = promisify(rimraf)
const readFile = promisify(fs.readFile)
const exists = promisify(fs.exists)

export interface UpOptions {
  preview?: boolean
  n?: number
  short?: boolean
  verbose?: boolean
  autoApprove?: boolean
  onWarnings?: (warnings: EngineResults.Warning[]) => Promise<boolean>
}
export interface DownOptions {
  n?: number
}

export interface WatchOptions {
  preview?: boolean
  providerAliases: ProviderAliases
  clear?: boolean
  autoApprove?: boolean
  onWarnings?: (warnings: EngineResults.Warning[]) => Promise<boolean>
}
interface MigrationFileMapOptions {
  migration: LocalMigrationWithDatabaseSteps
  lastMigration?: Migration
}
const brightGreen = chalk.rgb(127, 224, 152)

export class Lift {
  get devMigrationsDir() {
    return path.join(this.projectDir, 'migrations/dev')
  }
  public engine: LiftEngine

  // tslint:disable
  public watchUp = simpleDebounce(
    async (
      { preview, providerAliases, clear, onWarnings, autoApprove }: WatchOptions = { clear: true, providerAliases: {} },
      renderer?: DevComponentRenderer,
    ) => {
      debug('Running watchUp')
      renderer && renderer.setState({ error: undefined })
      const datamodel = await this.getDatamodel()
      try {
        const watchMigrationName = `watch-${now()}`
        const migration = await this.createMigration(watchMigrationName)
        const existingWatchMigrations = await this.getLocalWatchMigrations()

        if (migration && migration.warnings && migration.warnings.length > 0 && onWarnings && !autoApprove) {
          // if (migration?.warnings && onWarnings) { As soon as ts-node uses TS 3.7
          const ok = await onWarnings(migration.warnings)
          if (!ok) {
            await exit()
          }
        }

        if (migration) {
          debug('There is a migration we are going to apply now')
          const before = Date.now()
          renderer && renderer.setState({ lastChanged: new Date() })
          renderer && renderer.setState({ migrating: true })
          await this.engine.applyMigration({
            force: true,
            migrationId: migration.id,
            steps: migration.datamodelSteps,
            sourceConfig: datamodel,
          })
          debug(`Applied migration`)
          const lastWatchMigration =
            existingWatchMigrations.length > 0 ? existingWatchMigrations[existingWatchMigrations.length - 1] : undefined

          await this.persistWatchMigration({ migration, lastMigration: lastWatchMigration })
          const after = Date.now()
          renderer && renderer.setState({ migrating: false, migratedIn: after - before })
          if (renderer) {
            this.recreateStudioServer(providerAliases)
          }
        } else {
          debug(`No migration to apply`)
        }

        if (datamodel !== this.datamodelBeforeWatch) {
          renderer &&
            renderer.setState({
              datamodelBefore: this.datamodelBeforeWatch,
              datamodelAfter: datamodel,
            })
        }

        const generators = await getGenerators({
          schemaPath: getDatamodelPath(this.projectDir),
          printDownloadProgress: false,
          version: packageJson.prisma.version,
          cliVersion: packageJson.version,
        })

        const newGenerators = generators.map(gen => ({
          name: (gen.manifest ? gen.manifest.prettyName : gen.options!.generator.provider) || 'Generator',
          generatedIn: undefined,
          generating: false,
        }))

        const addedGenerators = newGenerators.filter(
          g => renderer && !renderer.state.generators.some(gg => gg.name === g.name),
        )
        const removedGenerators =
          (renderer && renderer.state.generators.filter(g => newGenerators.some(gg => gg.name === g.name))) || []

        if (
          (renderer && renderer.state.generators.length !== newGenerators.length) ||
          addedGenerators.length > 0 ||
          removedGenerators.length > 0
        ) {
          renderer && renderer.setState({ generators: newGenerators })
        }

        const version = packageJson.name === 'prisma2' ? packageJson.version : null

        for (let i = 0; i < generators.length; i++) {
          const generator = generators[i]
          if (
            version &&
            generator.manifest?.version &&
            generator.manifest?.version !== version &&
            generator.options?.generator.provider === 'photonjs'
          ) {
            console.error(
              `${chalk.bold(`@prisma/photon@${generator.manifest?.version}`)} is not compatible with ${chalk.bold(
                `prisma2@${version}`,
              )}. Their versions need to be equal.`,
            )
          }

          const before = Date.now()
          renderer &&
            renderer.setGeneratorState(i, {
              generating: true,
            })
          try {
            debug(`Generating ${generator.manifest!.prettyName}`)
            await generator.generate()
            generator.stop()
            const after = Date.now()
            renderer &&
              renderer.setGeneratorState(i, {
                generating: false,
                generatedIn: after - before,
              })
          } catch (error) {
            renderer && renderer.setState({ error })
          }
        }
      } catch (error) {
        renderer && renderer.setState({ error })
      }
    },
  )
  // tsline:enable
  private datamodelBeforeWatch: string = ''
  private studioServer?: Studio
  private studioPort: number = 5555
  private projectDir: string
  constructor(projectDir?: string) {
    this.projectDir = projectDir || this.getSchemaDir()
    const schemaPath = getDatamodelPath(this.projectDir)
    this.engine = new LiftEngine({ projectDir: this.projectDir, schemaPath })
  }

  public getSchemaDir(): string {
    const schemaPath = getSchemaDirSync()
    if (!schemaPath) {
      throw new Error(`Could not find schema.prisma`)
    }

    return schemaPath
  }

  public getDatamodel(): string {
    return fs.readFileSync(getDatamodelPath(this.projectDir), 'utf-8')
  }

  // TODO: optimize datapaths, where we have a datamodel already, use it
  public getSourceConfig(): string {
    return this.getDatamodel()
  }

  public async recreateStudioServer(providerAliases: ProviderAliases) {
    try {
      if (this.studioServer) {
        return await this.studioServer.restart(providerAliases)
      }

      this.studioServer = new Studio({ projectDir: this.projectDir, port: this.studioPort })
      await this.studioServer.start(providerAliases)
    } catch (e) {
      debug(e)
    }
  }

  public async getLockFile(): Promise<LockFile> {
    const lockFilePath = path.resolve(this.projectDir, 'migrations', 'lift.lock')
    if (await exists(lockFilePath)) {
      const file = await readFile(lockFilePath, 'utf-8')
      const lockFile = deserializeLockFile(file)
      if (lockFile.remoteBranch) {
        // TODO: Implement handling the conflict
        throw new Error(
          `There's a merge conflict in the ${chalk.bold(
            'migrations/lift.lock',
          )} file. Please execute ${chalk.greenBright('prisma lift fix')} to solve it`,
        )
      }
      return lockFile
    }

    return initLockFile()
  }

  public async createMigration(migrationId: string): Promise<LocalMigrationWithDatabaseSteps | undefined> {
    const { migrationsToApply, sourceConfig } = await this.getMigrationsToApply()

    const assumeToBeApplied = flatMap(migrationsToApply, m => m.datamodelSteps)

    const datamodel = await this.getDatamodel()
    const { datamodelSteps, databaseSteps, warnings } = await this.engine.inferMigrationSteps({
      sourceConfig,
      datamodel,
      migrationId,
      assumeToBeApplied,
    })

    if (datamodelSteps.length === 0) {
      debug(`No datamodelSteps`)
      debug(`No datamodelSteps`)
      debug(`No datamodelSteps`)
      debug(`No datamodelSteps`)
      debug(`No datamodelSteps`)
      debug(`No datamodelSteps`)
      debug({ datamodelSteps, databaseSteps, warnings })
      return undefined
    }

    return {
      id: migrationId,
      datamodel,
      datamodelSteps,
      databaseSteps,
      warnings,
    }
  }

  public getMigrationId(name?: string) {
    const timestamp = now()
    return timestamp + (name ? `-${dashify(name)}` : '')
  }

  public async save(
    migration: LocalMigrationWithDatabaseSteps,
    name?: string,
    preview?: boolean,
  ): Promise<{ files: FileMap; migrationId: string; newLockFile: string }> {
    const migrationId = this.getMigrationId(name)
    migration.id = migrationId
    const lockFile = await this.getLockFile()
    const { datamodel } = migration
    const localMigrations = await this.getLocalMigrations()
    const lastMigration = localMigrations.length > 0 ? localMigrations[localMigrations.length - 1] : undefined

    // TODO better printing of params
    const nameStr = name ? ` --name ${chalk.bold(name)}` : ''
    const previewStr = preview ? ` --preview` : ''
    console.log(`📼  lift save${nameStr}${previewStr}`)
    if (lastMigration) {
      const wording = preview ? `Potential datamodel changes:` : 'Local datamodel Changes:'
      console.log(chalk.bold(`\n${wording}\n`))
    } else {
      console.log(brightGreen.bold('\nNew datamodel:\n'))
    }
    if (lastMigration) {
      console.log(printDatamodelDiff(lastMigration.datamodel, datamodel))
    } else {
      console.log(highlightDatamodel(datamodel))
    }

    lockFile.localMigrations.push(migrationId)
    const newLockFile = serializeLockFile(lockFile)

    await del(this.devMigrationsDir)

    return {
      migrationId,
      files: this.getMigrationFileMap({ migration, lastMigration }),
      newLockFile,
    }
  }

  public async getLocalWatchMigrations(): Promise<Migration[]> {
    return this.getLocalMigrations(this.devMigrationsDir)
  }

  public async watch(options: WatchOptions = { preview: false, clear: true, providerAliases: {} }): Promise<string> {
    if (!options.clear) {
      options.clear = true
    }

    const datamodel = await this.getDatamodel()

    const generators = await getGenerators({
      schemaPath: getDatamodelPath(this.projectDir),
      printDownloadProgress: false,
      version: packageJson.prisma.version,
      cliVersion: packageJson.version,
    })

    this.studioPort = await getPort({ port: getPort.makeRange(5555, 5600) })

    const datamodelPath = getDatamodelPath(this.projectDir)
    const relativeDatamodelPath = path.relative(process.cwd(), datamodelPath)

    // From here on, we render the dev ui
    const renderer = new DevComponentRenderer({
      port: this.studioPort,
      initialState: {
        studioPort: this.studioPort,
        datamodelBefore: this.datamodelBeforeWatch,
        datamodelAfter: datamodel,
        generators: generators.map(gen => ({
          name: (gen.manifest ? gen.manifest.prettyName : gen.options!.generator.provider) || 'Generator',
          generatedIn: undefined,
          generating: false,
        })),
        datamodelPath,
        migrating: false,
        migratedIn: undefined,
        lastChanged: undefined,
        relativeDatamodelPath,
      },
    })

    // silent everyone else. this is not a democracy 👹
    console.log = (...args) => {
      debug(...args)
    }

    // console.error = (...args) => {
    //   debug(...args)
    // }

    this.recreateStudioServer(options.providerAliases)

    const { migrationsToApply } = await this.getMigrationsToApply()

    if (migrationsToApply.length > 0) {
      renderer.setState({ migrating: true }) // TODO: Show that this is actually applying real migrations, not just watch migrations
      // TODO: Ask for permission if we actually want to do it?
      // console.log(`Applying unapplied migrations ${chalk.blue(migrationsToApply.map(m => m.id).join(', '))}\n`)
      await this.up({
        short: true,
        autoApprove: options.autoApprove,
        onWarnings: async warnings => renderer.promptForWarnings(warnings),
      })
      // console.log(`Done applying migrations in ${formatms(Date.now() - before)}`)
      options.clear = false
      renderer.setState({ migrating: false })
    }

    options.onWarnings = warnings => renderer.promptForWarnings(warnings)

    const localMigrations = await this.getLocalMigrations()
    const watchMigrations = await this.getLocalWatchMigrations()

    let lastChanged: undefined | Date
    if (watchMigrations.length > 0) {
      const timestamp = watchMigrations[watchMigrations.length - 1].id.split('-')[1]
      lastChanged = timestampToDate(timestamp)
    } else if (localMigrations.length > 0) {
      lastChanged = timestampToDate(localMigrations[localMigrations.length - 1].id.split('-')[0])
    }
    renderer.setState({ lastChanged })

    if (localMigrations.length > 0) {
      this.datamodelBeforeWatch = localMigrations[localMigrations.length - 1].datamodel
      renderer.setState({
        datamodelBefore: this.datamodelBeforeWatch,
      })
    }

    await makeDir(this.devMigrationsDir)

    fs.watch(getDatamodelPath(this.projectDir), (eventType, filename) => {
      if (eventType === 'change') {
        this.watchUp(options, renderer)
      }
    })

    this.watchUp(options, renderer)
    return ''
  }

  public async down({ n }: DownOptions): Promise<string> {
    await this.getLockFile()
    const before = Date.now()
    const localMigrations = await this.getLocalMigrations()
    const localWatchMigrations = await this.getLocalWatchMigrations()
    if (localWatchMigrations.length > 0) {
      throw new Error(
        `Before running ${chalk.yellow('prisma migrate down --experimental')}, please save your ${chalk.bold(
          'dev',
        )} changes using ${chalk.bold.greenBright('prisma migrate create --experimental')} and ${chalk.bold.greenBright(
          'prisma2 migrate up --experimental',
        )}`,
      )
    }
    const datamodel = await this.getDatamodel()
    const appliedRemoteMigrations = await this.engine.listAppliedMigrations({ sourceConfig: datamodel })

    // TODO cleanup
    let lastAppliedIndex = -1
    const appliedMigrations = localMigrations.filter((localMigration, index) => {
      const remoteMigration = appliedRemoteMigrations[index]
      // if there is already a corresponding remote migration,
      // we don't need to apply this migration

      if (remoteMigration) {
        if (
          localMigration.id !== remoteMigration.id &&
          !isWatchMigrationName(remoteMigration.id) // it's fine to have the watch migration remotely
        ) {
          throw new Error(
            `Local and remote migrations are not in lockstep. We have migration ${localMigration.id} locally and ${remoteMigration.id} remotely at the same position in the history.`,
          )
        }
        lastAppliedIndex = index
        return true
      }
      return false
    })

    if (lastAppliedIndex === -1) {
      return 'No migration to roll back'
    }

    if (n && n > appliedMigrations.length) {
      throw new Error(
        `You provided ${chalk.redBright(`n = ${chalk.bold(String(n))}`)}, but there are only ${
          appliedMigrations.length
        } applied migrations that can be rolled back. Please provide ${chalk.green(
          String(appliedMigrations.length),
        )} or lower.`,
      )
    }

    n = n || 1

    for (let i = 0; i < n; i++) {
      const lastApplied = localMigrations[lastAppliedIndex]
      console.log(`Rolling back migration ${blue(lastApplied.id)}`)

      const result = await this.engine.unapplyMigration({ sourceConfig: datamodel })

      if (result.errors && result.errors.length > 0) {
        throw new Error(`Errors during rollback: ${JSON.stringify(result.errors)}`)
      }

      lastAppliedIndex--
    }

    return `🚀 Done with ${chalk.bold('down')} in ${formatms(Date.now() - before)}`
  }

  public async up({ n, preview, short, verbose, autoApprove, onWarnings }: UpOptions = {}): Promise<string> {
    await this.getLockFile()
    const before = Date.now()

    const migrationsToApplyResult = await this.getMigrationsToApply()
    const { lastAppliedIndex, localMigrations, appliedRemoteMigrations, sourceConfig } = migrationsToApplyResult
    let { migrationsToApply } = migrationsToApplyResult

    if (typeof n === 'number') {
      migrationsToApply = migrationsToApply.slice(0, n)
    }

    if (!short) {
      const previewStr = preview ? ` --preview` : ''
      console.log(`🏋 lift up${previewStr}\n`)

      if (migrationsToApply.length === 0) {
        return 'All migrations are already applied'
      }

      const lastAppliedMigration: Migration | undefined =
        lastAppliedIndex > -1 ? localMigrations[lastAppliedIndex] : undefined
      const lastUnappliedMigration: Migration = migrationsToApply.slice(-1)[0]

      if (lastUnappliedMigration.datamodel.length < 10000) {
        if (lastAppliedMigration) {
          console.log(chalk.bold('Changes to be applied:') + '\n')
          console.log(printDatamodelDiff(lastAppliedMigration.datamodel, lastUnappliedMigration.datamodel))
        } else {
          console.log(brightGreen.bold('Datamodel that will initialize the db:\n'))
          console.log(highlightDatamodel(lastUnappliedMigration.datamodel))
        }
      }
    }

    console.log(`\nChecking the datasource for potential data loss...`)
    const firstMigrationToApplyIndex = localMigrations.indexOf(migrationsToApply[0])
    const migrationsWithDbSteps = await this.getDatabaseSteps(localMigrations, firstMigrationToApplyIndex, sourceConfig)

    const warnings = flatMap(migrationsWithDbSteps, m => m.warnings)

    if (warnings.length > 0 && !autoApprove) {
      if (onWarnings && typeof onWarnings === 'function' && !autoApprove) {
        const ok = await onWarnings(warnings)
        if (!ok) {
          await exit()
        }
      }
      console.log(chalk.bold(`\n\n⚠  There will be data loss:\n`))
      for (const warning of warnings) {
        console.log(`  • ${warning.description}`)
      }
      console.log() // empty line before prompt
      if (!autoApprove && !onWarnings) {
        const response = await prompt({
          type: 'confirm',
          name: 'value',
          message: `Are you sure you want to apply this change?`,
        })

        if (!response.value) {
          await exit()
        }
      } else {
        console.log(`As ${chalk.bold('--auto-approve')} is provided, the destructive changes are accepted.\n`)
      }
    }

    const progressRenderer = new ProgressRenderer(migrationsWithDbSteps, short || false)

    progressRenderer.render()

    if (preview) {
      await progressRenderer.done()
      return `\nTo apply the migrations, run ${chalk.greenBright('prisma2 migrate up --experimental')}\n`
    }

    for (let i = 0; i < migrationsToApply.length; i++) {
      const migrationToApply = migrationsToApply[i]
      const { id, datamodelSteps } = migrationToApply
      const result = await this.engine.applyMigration({
        force: true,
        migrationId: id,
        steps: datamodelSteps,
        sourceConfig,
      })
      await new Promise(r => setTimeout(r, 50))
      // needed for the ProgressRenderer
      // and for verbose printing
      migrationsWithDbSteps[i].databaseSteps = result.databaseSteps
      const totalSteps = result.databaseSteps.length
      let progress: EngineResults.MigrationProgress | undefined
      progressLoop: while (
        // tslint:disable-next-line
        (progress = await this.engine.migrationProgess({
          migrationId: id,
          sourceConfig,
        }))
      ) {
        if (progress.status === 'MigrationInProgress') {
          progressRenderer.setProgress(i, progress.applied / totalSteps)
        }
        if (progress.status === 'MigrationSuccess') {
          progressRenderer.setProgress(i, 1)
          break progressLoop
        }
        if (progress.status === 'RollbackSuccess') {
          cliCursor.show()
          throw new Error(`Rolled back migration. ${JSON.stringify(progress)}`)
        }
        if (progress.status === 'RollbackFailure') {
          cliCursor.show()
          throw new Error(`Failed to roll back migration. ${JSON.stringify(progress)}`)
        }
        await new Promise(r => setTimeout(r, 1500))
      }

      if (migrationToApply.afterFilePath) {
        const after = migrationToApply.afterFilePath
        plusX(after)
        const child = spawn(after, {
          env: {
            ...process.env,
            FORCE_COLOR: '1',
          },
        })
        child.on('error', e => {
          console.error(e)
        })
        child.stderr.on('data', d => {
          console.log(`stderr ${d.toString()}`)
        })
        progressRenderer.showLogs(path.basename(after), child.stdout)
        await new Promise(r => {
          child.on('close', () => {
            r()
          })
          child.on('exit', () => {
            r()
          })
        })
      }
    }
    await progressRenderer.done()

    if (verbose) {
      console.log(chalk.bold(`\nSQL Commands:\n`))
      console.log(highlightMigrationsSQL(migrationsWithDbSteps))
      console.log('\n')
    }

    return `\n🚀  Done with ${migrationsToApply.length} migration${
      migrationsToApply.length > 1 ? 's' : ''
    } in ${formatms(Date.now() - before)}.\n`
  }

  public stop() {
    this.engine.stop()
  }

  private getMigrationFileMap({ migration, lastMigration }: MigrationFileMapOptions): FileMap {
    // const { version } = packageJson
    const { datamodelSteps, datamodel } = migration

    return {
      ['steps.json']: JSON.stringify({ version: '0.3.14-fixed', steps: datamodelSteps }, null, 2),
      ['schema.prisma']: maskSchema(datamodel),
      ['README.md']: printMigrationReadme({
        migrationId: migration.id,
        lastMigrationId: lastMigration ? lastMigration.id : '',
        datamodelA: lastMigration ? lastMigration.datamodel : '',
        datamodelB: datamodel,
        databaseSteps: migration.databaseSteps,
      }),
    }
  }

  private async persistWatchMigration(options: MigrationFileMapOptions) {
    const fileMap = this.getMigrationFileMap(options)
    await serializeFileMap(fileMap, path.join(this.devMigrationsDir, options.migration.id))
  }

  private async getLocalMigrations(
    migrationsDir = path.join(this.projectDir, 'migrations'),
  ): Promise<LocalMigration[]> {
    if (!(await exists(migrationsDir))) {
      return []
    }
    const migrationSteps = await globby(
      [
        '**/steps.json',
        '**/schema.prisma',
        '**/datamodel.prisma',
        '**/after.sh',
        '**/before.sh',
        '**/after.ts',
        '**/before.ts',
        '!dev',
      ],
      {
        cwd: migrationsDir,
      },
    ).then(files =>
      Promise.all(
        files.map(async fileName => ({
          fileName: fileName.split('/')[1],
          migrationId: fileName.split('/')[0],
          file: await readFile(path.join(migrationsDir, fileName), 'utf-8'),
        })),
      ),
    )

    migrationSteps.sort((a, b) => (a.migrationId < b.migrationId ? -1 : 1))

    const groupedByMigration = groupBy<any>(migrationSteps, step => step.migrationId) // todo fix types

    return Object.entries(groupedByMigration).map(([migrationId, files]) => {
      const stepsFile = files.find(f => f.fileName === 'steps.json')!
      const datamodelFile = files.find(f => f.fileName === 'datamodel.prisma' || f.fileName === 'schema.prisma')!
      const afterFile = files.find(f => f.fileName === 'after.sh' || f.fileName === 'after.ts')
      const beforeFile = files.find(f => f.fileName === 'before.sh' || f.fileName === 'before.ts')
      const stepsFileJson = JSON.parse(stepsFile.file)
      if (Array.isArray(stepsFileJson)) {
        throw new Error(
          `We changed the steps.json format - please delete your migrations folder and run prisma lift create again`,
        )
      }
      if (!stepsFileJson.steps) {
        throw new Error(`${stepsFile.fileName} is expected to have a .steps property`)
      }

      return {
        id: migrationId,
        datamodelSteps: stepsFileJson.steps,
        datamodel: datamodelFile.file,
        afterFilePath: afterFile ? path.resolve(migrationsDir, migrationId, afterFile.fileName) : undefined,
        beforeFilePath: beforeFile ? path.resolve(migrationsDir, migrationId, beforeFile.fileName) : undefined,
      }
    })
  }

  private async getDatabaseSteps(
    localMigrations: Migration[],
    fromIndex: number,
    sourceConfig: string,
  ): Promise<LocalMigrationWithDatabaseSteps[]> {
    const migrationsWithDatabaseSteps = await pMap(
      localMigrations,
      async (migration, index) => {
        if (index < fromIndex) {
          return {
            ...migration,
            databaseSteps: [],
            warnings: [],
          }
        }
        const stepsUntilNow = index > 0 ? flatMap(localMigrations.slice(0, index), m => m.datamodelSteps) : []
        const input = {
          assumeToBeApplied: stepsUntilNow,
          stepsToApply: migration.datamodelSteps,
          sourceConfig,
        }
        const { databaseSteps, warnings } = await this.engine.calculateDatabaseSteps(input)
        return {
          ...migration,
          databaseSteps,
          warnings,
        }
      },
      { concurrency: 1 },
    )

    return migrationsWithDatabaseSteps.slice(fromIndex)
  }

  private async getMigrationsToApply(): Promise<{
    localMigrations: LocalMigration[]
    lastAppliedIndex: number
    migrationsToApply: LocalMigration[]
    sourceConfig: string
    appliedRemoteMigrations: EngineResults.StoredMigration[]
  }> {
    const localMigrations = await this.getLocalMigrations()

    const sourceConfig = await this.getSourceConfig()
    const appliedRemoteMigrations = await this.engine.listAppliedMigrations({ sourceConfig })
    const appliedRemoteMigrationsWithoutWatch = appliedRemoteMigrations.filter(m => !isWatchMigrationName(m.id))

    if (appliedRemoteMigrationsWithoutWatch.length > localMigrations.length) {
      const localMigrationIds = localMigrations.map(m => m.id)
      const remoteMigrationIds = appliedRemoteMigrationsWithoutWatch.map(m => m.id)

      throw new Error(
        `There are more migrations in the database than locally. This must not happen. Local migration ids: ${localMigrationIds.join(
          ', ',
        )}. Remote migration ids: ${remoteMigrationIds.join(', ')}`,
      )
    }

    let lastAppliedIndex = -1
    const migrationsToApply = localMigrations.filter((localMigration, index) => {
      const remoteMigration = appliedRemoteMigrationsWithoutWatch[index]
      // if there is already a corresponding remote migration,
      // we don't need to apply this migration

      if (remoteMigration) {
        if (localMigration.id !== remoteMigration.id && !isWatchMigrationName(remoteMigration.id)) {
          throw new Error(
            `Local and remote migrations are not in lockstep. We have migration ${localMigration.id} locally and ${remoteMigration.id} remotely at the same position in the history.`,
          )
        }
        if (!isWatchMigrationName(remoteMigration.id)) {
          lastAppliedIndex = index
          return false
        }
      }
      return true
    })

    return {
      localMigrations,
      lastAppliedIndex,
      migrationsToApply,
      appliedRemoteMigrations,
      sourceConfig,
    }
  }
}

class ProgressRenderer {
  private currentIndex = 0
  private currentProgress = 0
  private statusWidth = 6
  private logsString = ''
  private logsName?: string
  private silent: boolean
  constructor(private migrations: LocalMigrationWithDatabaseSteps[], silent: boolean) {
    cliCursor.hide()
    this.silent = silent
  }

  public setMigrations(migrations: LocalMigrationWithDatabaseSteps[]) {
    this.migrations = migrations
    this.render()
  }

  public setProgress(index: number, progressPercentage: number) {
    const progress = Math.min(Math.floor(progressPercentage * this.statusWidth), this.statusWidth)

    this.currentIndex = index
    this.currentProgress = progress
    this.render()
  }

  public showLogs(name, stream: Readable) {
    this.logsName = name
    this.logsString = ''
    stream.on('data', data => {
      this.logsString += data.toString()
      this.render()
    })
  }

  public render() {
    if (this.silent) {
      return
    }
    const maxMigrationLength = this.migrations.reduce((acc, curr) => Math.max(curr.id.length, acc), 0)
    let maxStepLength = 0
    const rows = this.migrations
      .map(m => {
        const steps = printDatabaseStepsOverview(m.databaseSteps)
        maxStepLength = Math.max(stripAnsi(steps).length, maxStepLength)
        let scripts = ''
        if (m.beforeFilePath || m.afterFilePath) {
          if (m.beforeFilePath && m.afterFilePath) {
            const beforeStr = m.beforeFilePath ? `└─ ${path.basename(m.beforeFilePath)}\n` : ''
            const afterStr = m.afterFilePath ? `\n└─ ${path.basename(m.afterFilePath)}` : ''
            scripts = '\n' + indent(`${beforeStr}└─ ${blue('Datamodel migration')}${afterStr}`, 2)
          } else {
            const beforeStr = m.beforeFilePath ? `└─ ${path.basename(m.beforeFilePath)}\n` : ''
            const afterStr = m.afterFilePath ? `└─ ${path.basename(m.afterFilePath)}` : ''
            scripts = '\n' + indent(`${beforeStr}${afterStr}`, 2)
          }
        }
        return {
          line: `${blue(m.id)}${' '.repeat(maxMigrationLength - m.id.length + 2)}${steps}`,
          scripts,
        }
      })
      .map((m, index) => {
        const maxLength = maxStepLength + maxMigrationLength
        const paddingLeft = maxLength - stripAnsi(m.line).length + 2
        const newLine = m.line + ' '.repeat(paddingLeft) + '  '

        if (this.currentIndex > index || (this.currentIndex === index && this.currentProgress === this.statusWidth)) {
          return newLine + 'Done 🚀' + m.scripts
        } else if (this.currentIndex === index) {
          return newLine + '\u25A0'.repeat(this.currentProgress) + m.scripts
        }

        return newLine
      })
      .join('\n')

    const column1 = 'Migration'
    const column2 = 'Database actions'
    const column3 = 'Status'
    const header =
      chalk.underline(column1) +
      ' '.repeat(Math.max(0, maxMigrationLength - column1.length)) +
      '  ' +
      chalk.underline(column2) +
      ' '.repeat(Math.max(0, maxStepLength - column2.length + 2)) +
      chalk.underline(column3) +
      '\n\n'

    const changeOverview = header + rows

    let str = ''
    str += chalk.bold('\nDatabase Changes:\n\n')
    str += changeOverview

    str += chalk.dim(
      `\n\nYou can get the detailed db changes with ${chalk.greenBright(
        'prisma2 migreate up --experimental --verbose',
      )}\nOr read about them in the ./migrations/MIGRATION_ID/README.md`,
    )

    if (this.logsName && this.logsString.length > 0) {
      str +=
        '\n\n' +
        drawBox({
          height: Math.min(15, process.stdout.rows || 15),
          width: process.stdout.columns || 40,
          str: this.logsString,
          title: this.logsName,
        }) +
        '\n'
    }

    logUpdate(str)
  }

  public async done() {
    cliCursor.show()
  }
}


keep keep keep replace replace keep keep keep

import { DatabaseStep } from './types'

export const exampleDbSteps: DatabaseStep[] = [
  {
    RawSql: 'PRAGMA foreign_keys=OFF;',
    raw: '',
  },
  {
    CreateTable: {
      name: 'new_Post',
      columns: [
        {
          name: 'id',
          tpe: 'Int',
          required: true,
          foreign_key: null,
          raw: '',
        },
        {
          name: 'title',
          tpe: 'String',
          required: true,
          foreign_key: null,
          raw: '',
        },
        {
          name: 'anotherText',
          tpe: 'String',
          required: true,
          foreign_key: null,
          raw: '',
        },
        {
          name: 'text',
          tpe: 'String',
          required: true,
          foreign_key: null,
          raw: '',
        },
        {
          name: 'blog',
          tpe: 'Int',
          required: true,
          foreign_key: {
            table: 'Blog',
            column: 'id',
          },
          raw: '',
        },
      ],
      primary_columns: ['id'],
    },
    raw: '',
  },
  {
    raw: '',
    RawSql: 'INSERT INTO new_Post (id,title,text,blog) SELECT id,title,text,blog from Post',
  },
  {
    raw: '',
    DropTable: {
      name: 'Post',
    },
  },
  {
    raw: '',
    RenameTable: {
      name: 'new_Post',
      new_name: 'Post',
    },
  },
  {
    raw: '',
    RawSql: 'PRAGMA "migration_engine".foreign_key_check;',
  },
  {
    raw: '',
    RawSql: 'PRAGMA foreign_keys=ON;',
  },
]


keep keep keep keep keep replace keep keep keep keep keep

import debugLib from 'debug'
import fs from 'fs'
import getPort from 'get-port'
import path from 'path'

import { ProviderAliases } from '@prisma/sdk'
import { getSchemaDirSync } from '@prisma/cli'
import { getPlatform } from '@prisma/get-platform'

import { getDatamodelPath } from './utils/getDatamodelPath'

export interface StudioOptions {
  projectDir?: string
  port?: number
}

const debug = debugLib('Studio')
const packageJson = eval(`require('../package.json')`) // tslint:disable-line

export class Studio {
  private projectDir: string
  private instance?: any
  private port?: number

  constructor({ projectDir, port }: StudioOptions = {}) {
    this.projectDir = projectDir || this.getSchemaDir()
    this.port = port
  }

  public async start(providerAliases: ProviderAliases): Promise<string> {
    try {
      if (this.instance) {
        throw new Error(`Studio is already started`)
      }

      const platform = await getPlatform()
      const extension = platform === 'windows' ? '.exe' : ''

      const pathCandidates = [
        // ncc go home
        // tslint:disable-next-line
        eval(`require('path').join(__dirname, '../node_modules/@prisma/sdk/query-engine-${platform}${extension}')`), // for local dev
        // tslint:disable-next-line
        eval(`require('path').join(__dirname, '../query-engine-${platform}${extension}')`), // for production
      ]

      const pathsExist = await Promise.all(
        pathCandidates.map(async candidate => ({ exists: fs.existsSync(candidate), path: candidate })),
      )

      const firstExistingPath = pathsExist.find(p => p.exists)

      if (!firstExistingPath) {
        throw new Error(
          `Could not find any Prisma2 query-engine binary for Studio. Looked in ${pathCandidates.join(', ')}`,
        )
      }

      const StudioServer = (await import('@prisma/studio-server')).default

      let photonWorkerPath: string | undefined
      try {
        const studioTransport = require.resolve('@prisma/studio-transports')
        photonWorkerPath = path.join(path.dirname(studioTransport), 'photon-worker.js')
      } catch (e) {
        //
      }

      if (!this.port) {
        this.port = await getPort({ port: getPort.makeRange(5555, 5600) })
      }

      this.instance = new StudioServer({
        port: this.port,
        debug: false,
        binaryPath: firstExistingPath.path,
        photonWorkerPath,
        photonGenerator: {
          version: packageJson.prisma.version,
          providerAliases,
        },
        schemaPath: getDatamodelPath(this.projectDir),
        // reactAppDir: path.join(path.dirname(require.resolve('@prisma/studio/package.json')), 'build'),
      })

      await this.instance.start()

      return `Studio started at http://localhost:${this.port}`
    } catch (e) {
      debug(e)
    }

    return ''
  }

  public async restart(providerAliases: ProviderAliases): Promise<string> {
    if (this.instance) {
      await this.instance.restart()
      return ''
    }

    return this.start(providerAliases)
  }

  private getSchemaDir(): string {
    const schemaPath = getSchemaDirSync()
    if (!schemaPath) {
      throw new Error(`Could not find schema.prisma`)
    }

    return schemaPath
  }
}


function flatten(array) {
  return Array.prototype.concat.apply([], array)
}

export function flatMap<T, U>(
  array: T[],
  callbackFn: (value: T, index: number, array: T[]) => U[],
  thisArg?: any,
): U[] {
  return flatten(array.map(callbackFn, thisArg))
}


// export type GeneratorWorkerJob = {
//   packagePath: string
//   config: GeneratorConfig
// }
process.on('message', async message => {
  const job /*: GeneratorWorkerJob*/ = JSON.parse(message)
  try {
    const package = require(job.packagePath)
    const generatorFunction = package.generatorDefinition.generate
    await generatorFunction(job.config)
    process.exit(0)
  } catch (e) {
    process.send(JSON.stringify({ error: e.toString() }))
    process.exit(1)
  }
})


const StudioServer = (await import('@prisma/studio-server')).default

let photonWorkerPath: string | undefined

const studioTransport = require.resolve('@prisma/studio-transports')

photonWorkerPath = path.join(path.dirname(studioTransport), 'photon-worker.js')

this.port = await getPort({ port: getPort.makeRange(5555, 5600) })

keep keep keep keep replace replace replace replace replace replace replace replace keep keep keep keep


import fs from 'fs'
import path from 'path'

import { getSchemaPathSync } from '@prisma/cli'

export function getDatamodelPath(projectDir: string): string {
  if (projectDir) {
    if (fs.existsSync(path.join(projectDir, 'schema.prisma'))) {
      return path.join(projectDir, 'schema.prisma')
    }
    if (fs.existsSync(path.join(projectDir, 'prisma/schema.prisma'))) {
      return path.join(projectDir, 'prisma/schema.prisma')
    }
  }

  const schemaPath = getSchemaPathSync()
  if (!schemaPath) {
    throw new Error(`Could not find schema.prisma in ${projectDir}`)
  }

  return schemaPath
}


import { arg, Command, format, HelpError, isError } from '@prisma/cli'
import chalk from 'chalk'
import { DownOptions, Lift } from '../../Lift'
import { ensureDatabaseExists } from '../../utils/ensureDatabaseExists'
import { ExperimentalFlagError } from '../../utils/experimental'

export class LiftDown implements Command {
  public static new(): LiftDown {
    return new LiftDown()
  }

  // static help template
  private static help = format(`
    Migrate your database down to a specific state.

    ${chalk.bold('Usage')}

      prisma migrate down [<dec|name|timestamp>] --experimental

    ${chalk.bold('Arguments')}

      [<dec>]   go down by an amount [default: 1]

    ${chalk.bold('Options')}

      -h, --help       Displays this help message
      -p, --preview    Preview the migration changes

    ${chalk.bold('Examples')}

      Preview a migration without migrating
      ${chalk.dim(`$`)} prisma migrate down --preview --experimental

      Rollback a migration
      ${chalk.dim(`$`)} prisma migrate down 1 --experimental

      Go down to a migration by timestamp
      ${chalk.dim(`$`)} prisma migrate down 20190605204907 --experimental

      Go down to a migration by name
      ${chalk.dim(`$`)} prisma migrate down "add first_name field" --experimental
  `)
  private constructor() {}

  // parse arguments
  public async parse(argv: string[]): Promise<string | Error> {
    // parse the arguments according to the spec
    const args = arg(argv, {
      '--help': Boolean,
      '-h': '--help',
      '--preview': Boolean,
      '-p': '--preview',
      '--experimental': Boolean,
    })

    if (!args['--experimental']) {
      throw new ExperimentalFlagError()
    }

    if (isError(args)) {
      return this.help(args.message)
    } else if (args['--help']) {
      return this.help()
    }

    const lift = new Lift()

    const options: DownOptions = {}

    // TODO add go down by name and timestamp
    if (args._.length > 0) {
      const thisArg = args._[0]
      const maybeNumber = parseInt(thisArg, 10)

      // in this case it's a migration id
      if (isNaN(maybeNumber) || typeof maybeNumber !== 'number') {
        throw new Error(`Invalid migration step ${maybeNumber}`)
      } else {
        options.n = maybeNumber
      }
    }

    await ensureDatabaseExists('unapply', true)

    const result = await lift.down(options)
    lift.stop()
    return result
  }

  // help message
  public help(error?: string): string | HelpError {
    if (error) {
      return new HelpError(`\n${chalk.bold.red(`!`)} ${error}\n${LiftDown.help}`)
    }
    return LiftDown.help
  }
}


import chalk from 'chalk'

export function printMigrationId(migrationId: string) {
  const words = migrationId.split('-')
  if (words.length === 1) {
    return chalk.cyan.bold(migrationId)
  }
  return `${words[0]}-${chalk.cyan.bold(words.slice(1).join('-'))}`
}


keep keep keep keep keep replace replace replace replace replace replace replace keep

@prisma/lift

prettier --write src/*.{ts,tsx,js,jsx,md,graphql} src/**/*.{ts,tsx,js,jsx,md,graphql} && tsc

tsc -d && cp src/GeneratorWorker.js dist/GeneratorWorker.js

pnpm run build && pnpm run ncc && sed -i.bak 's/env ts-node/env node/g' 'build/index.js'

tslint --project tsconfig.json -c tslint.json --fix


import execa from 'execa'

export async function runGeneratorBinary(
  binaryPath: string,
  options: any,
): Promise<{ stdout: string; stderr: string }> {
  const child = execa(binaryPath)

  child.stdin!.write(JSON.stringify(options) + '\n')

  return await child
}


export type SyntaxCondition =
  | RegExp
  | {
      pattern: RegExp
      greedy?: boolean
      lookbehind?: boolean
    }

export type SyntaxDefinition = {
  comment?: SyntaxCondition | Array<SyntaxCondition>
  variable?: SyntaxCondition | Array<SyntaxCondition>
  string?: SyntaxCondition | Array<SyntaxCondition>
  function?: SyntaxCondition | Array<SyntaxCondition>
  keyword?: SyntaxCondition | Array<SyntaxCondition>
  boolean?: SyntaxCondition | Array<SyntaxCondition>
  number?: SyntaxCondition | Array<SyntaxCondition>
  operator?: SyntaxCondition | Array<SyntaxCondition>
  punctuation?: SyntaxCondition | Array<SyntaxCondition>
  directive?: SyntaxCondition | Array<SyntaxCondition>
  entity?: SyntaxCondition | Array<SyntaxCondition>
  value?: SyntaxCondition | Array<SyntaxCondition>
  field?: SyntaxCondition | Array<SyntaxCondition>
}

export type Theme = {
  comment: (str: string) => string
  variable?: (str: string) => string
  string?: (str: string) => string
  function?: (str: string) => string
  keyword?: (str: string) => string
  boolean?: (str: string) => string
  number?: (str: string) => string
  operator?: (str: string) => string
  punctuation?: (str: string) => string
  directive?: (str: string) => string
  entity?: (str: string) => string
  value?: (str: string) => string
  field?: (str: string) => string
}


// reactAppDir: path.join(path.dirname(require.resolve('@prisma/studio/package.json')), 'build'),

return `Studio started at http://localhost:${this.port}`


import { TabIndexProvider, TextInput } from '@prisma/ink-components'
import chalk from 'chalk'
import { Box, Color } from 'ink'
import React, { useState } from 'react'
import { EngineResults } from '../types'

export interface WarningsPromptProps {
  warnings: EngineResults.Warning[]
  onSubmitWarningsPrompt: (ok: boolean) => void
}

export const WarningsPrompt: React.FC<WarningsPromptProps> = ({ warnings, onSubmitWarningsPrompt }) => {
  const [value, setValue] = useState('')

  const handleChange = (newValue: string) => {
    if (newValue.toLocaleLowerCase() === 'y') {
      setValue(newValue)
      onSubmitWarningsPrompt(true)
    }
    if (newValue.toLowerCase() === 'n') {
      setValue(newValue)
      onSubmitWarningsPrompt(false)
    }
    if (newValue === 'enter') {
      // coming from onSubmit
      setValue('N')
      onSubmitWarningsPrompt(false)
    }
  }

  return (
    <TabIndexProvider>
      <Box flexDirection="column">
        <Color bold>⚠{'  '} There will be data loss:</Color>
        <Box marginTop={1} marginBottom={1} marginLeft={2} flexDirection="column">
          {warnings.map(warning => (
            <Box>• {warning.description}</Box>
          ))}
        </Box>
        <TextInput
          value={value}
          onChange={handleChange}
          onSubmit={() => handleChange('enter')}
          label={'Are you sure you want to apply this change? ' + chalk.dim('y/N')}
          tabIndex={0}
        />
      </Box>
    </TabIndexProvider>
  )
}


import psTree from 'ps-tree'

export async function exit() {
  const children = await getChildProcesses()
  children.forEach(child => {
    process.kill(Number(child.PID))
  })
  process.exit()
}

interface ChildProcessInfo {
  PPID: string
  PID: string
  STAT: string
  COMM: string
}

function getChildProcesses(): Promise<ChildProcessInfo[]> {
  return new Promise((resolve, reject) => {
    psTree(process.pid, (err, children) => {
      if (err) {
        reject(err)
      } else {
        resolve(children)
      }
    })
  })
}


import cliCursor from 'cli-cursor'
import { Box, Color, StdinContext, StdoutContext } from 'ink'
import Link from 'ink-link'
import React, { Component } from 'react'
import stripAnsi from 'strip-ansi'
import supportsHyperlinks from 'supports-hyperlinks'
import { EngineResults } from '../types'
import { formatms } from '../utils/formartms'
import { missingGeneratorMessage } from '../utils/generation/missingGeneratorMessage'
import { renderDate } from '../utils/now'
import { printDatamodelDiff } from '../utils/printDatamodelDiff'
import { Spinner } from './Spinner'
import { WarningsPrompt } from './WarningsPrompt'

export interface GeneratorInfo {
  name: string
  generatedIn: number | undefined
  generating: boolean
}

export interface DevComponentProps {
  datamodelBefore: string
  datamodelAfter: string
  migrating: boolean
  migratedIn: number | undefined
  generators: GeneratorInfo[]
  lastChanged: Date | undefined
  error?: Error
  datamodelPath: string
  studioPort: number
  relativeDatamodelPath: string
  warnings?: EngineResults.Warning[]
  onSubmitWarningsPrompt?: (ok: boolean) => void
}

export interface Props extends DevComponentProps {
  stdout: NodeJS.WriteStream
  stdin: NodeJS.ReadStream
  setRawMode: ((mode: boolean) => void) | undefined
}

export interface State {
  showDiff: boolean
  diffScroll: number
  error?: Error
}
// const ARROW_UP = '\u001B[A'
// const ARROW_DOWN = '\u001B[B'
const ARROW_LEFT = '\u001B[D'
const ARROW_RIGHT = '\u001B[C'
const ENTER = '\r'
// const CTRL_C = '\x03'
const BACKSPACE = '\x08'
const DELETE = '\x7F'

class DevComponent extends Component<Props, State> {
  get width() {
    return (this.props.stdout.columns || 64) - 4
  }
  get height() {
    return Math.max(Math.min(process.stdout.rows || 19, 120) - 2, 10)
  }
  get diff(): { diff: string; rowCount: number } {
    return this.getDiff()
  }
  public static getDerivedStateFromError(error: Error) {
    return { error }
  }
  public resizeInterval: any
  public state: State = {
    showDiff: false,
    diffScroll: 0,
    error: undefined,
  }
  public getDiff = () => {
    const { datamodelBefore, datamodelAfter } = this.props
    const diff = printDatamodelDiff(datamodelBefore, datamodelAfter)
    return { diff, rowCount: stripAnsi(diff).split('\n').length }
  }
  public componentDidMount() {
    cliCursor.hide()
    process.stdout.on('resize', this.handleWindowResize)
    this.props.stdin.on('data', this.handleStdinData)

    if (this.props.setRawMode) {
      this.props.setRawMode(true)
    }
  }

  public componentWillUnmount() {
    cliCursor.show()

    process.stdout.off('resize', this.handleWindowResize)
    this.props.stdin.off('data', this.handleStdinData)

    if (this.props.setRawMode) {
      this.props.setRawMode(false)
    }
  }

  private handleWindowResize = () => {
    this.forceUpdate()
  }

  private handleStdinData = (data: Buffer) => {
    const key = String(data)
    if (key === 'b') {
      this.setState({ showDiff: false })
    }

    if (key === 'd' || key === ARROW_LEFT || key === ARROW_RIGHT) {
      this.setState(state => ({ ...state, showDiff: !state.showDiff }))
    }

    if (this.state.showDiff) {
      if (key === ENTER || key === BACKSPACE || key === DELETE) {
        this.setState({ showDiff: false })
      }

      // TODO: Implement scrolling view
    }
  }

  public trimmedDiff() {
    return this.diff.diff
    // const { diff, rowCount } = this.diff
    // const { diffScroll } = this.state
    // if (diffScroll === 0) {
    //   return diff
    // }
    // const lines = diff.split('\n')
    // if (diffScroll > 0) {
    //   return lines.slice(diffScroll).join('\n')
    // }
    // return lines.slice(0, rowCount + diffScroll).join('\n')
  }

  public render() {
    if (this.state.error) {
      return (
        <Box flexDirection="column">
          <Color bold red>
            Unexpected Error in dev command
          </Color>
          <Color red>{this.state.error.stack || this.state.error.message}</Color>
        </Box>
      )
    }

    const { generators, warnings, onSubmitWarningsPrompt } = this.props
    const smallScreen = this.height <= 16
    const paddingTop = smallScreen ? 1 : 2
    if (this.state.showDiff) {
      const { diff, rowCount } = this.diff
      const height = Math.max(this.height, rowCount + 5)
      return (
        <Box flexDirection="column" marginTop={1} justifyContent="space-between" height={height}>
          <Box flexDirection="column">
            <Box>
              <Color>
                Changes in datamodel since last <Color bold>prisma2 migrate save</Color>
              </Color>
            </Box>
            <Box marginTop={1} marginLeft={2}>
              {diff.trim() === '' ? `No unsaved changes` : this.trimmedDiff()}
            </Box>
          </Box>
          <Box flexDirection="column">
            <Color dim>{'─'.repeat(this.width)}</Color>
            <Box>
              <Box>
                <Color bold>b: </Color>
                <Color dim>back</Color>
              </Box>
            </Box>
          </Box>
        </Box>
      )
    }
    if (this.props.error) {
      const error: any = this.props.error
      let kind = ''
      let callsite: Element | undefined
      if (error.code && error.code === 1001) {
        kind = 'Datamodel Validation '
        callsite = (
          <Box marginLeft={1}>
            <Color redBright>
              in <Color underline>{this.props.datamodelPath}</Color>
            </Color>
          </Box>
        )
      }

      return (
        <Box flexDirection="column" marginTop={1} justifyContent="flex-start">
          <Box>
            <Color bold redBright>
              {kind}Error
            </Color>
            {callsite}
          </Box>
          <Box marginTop={1}>
            <Color>{this.props.error.stack}</Color>
          </Box>
        </Box>
      )
    }
    return (
      <Box flexDirection="column" marginTop={1} marginLeft={1} height={this.height} justifyContent="space-between">
        <Box flexDirection="column">
          <Box marginLeft={2} flexDirection="column">
            <Color bold>Watching for changes in {this.props.relativeDatamodelPath}</Color>
            {this.props.lastChanged ? (
              <Color dim>Last changed at {renderDate(this.props.lastChanged)}</Color>
            ) : (
              <Color dim>No changes yet</Color>
            )}
          </Box>
          <Box marginLeft={2} marginTop={paddingTop} flexDirection="column">
            <Color bold>Migrations</Color>
            {!smallScreen && <Color dim>{'─'.repeat(this.width)}</Color>}
          </Box>
          <Box marginTop={0} flexDirection="column">
            {this.props.migrating ? (
              <Color yellow>
                <Spinner type="dots10" />{' '}
                {warnings && warnings.length > 0 ? 'Waiting for confirmation...' : 'Migrating the database...'}
              </Color>
            ) : (
              <Color green>
                ✓ Database successfully migrated
                {this.props.migratedIn ? <> in {formatms(this.props.migratedIn)}</> : null}
              </Color>
            )}
            {this.diff.diff.trim() !== '' && (
              <Box marginLeft={2}>
                <Color dim>
                  To save changes into a migration file, run{' '}
                  <Color bold dim green>
                    prisma2 migrate save --experimental
                  </Color>
                </Color>
              </Box>
            )}
          </Box>
          <Box marginLeft={2} marginTop={paddingTop} flexDirection="column">
            <Color bold>Generators</Color>
            {!smallScreen && <Color dim>{'─'.repeat(this.width)}</Color>}
          </Box>
          <Box marginTop={0}>
            {generators.length === 0 ? (
              <Box height={missingGeneratorMessage.split('\n').length} marginLeft={4}>
                {missingGeneratorMessage}
              </Box>
            ) : (
              <>
                <Box flexDirection="column">
                  {generators.map(gen =>
                    gen.generating ? (
                      <Color yellow key={gen.name}>
                        <Spinner type="dots10" /> {gen.name}
                      </Color>
                    ) : gen.generatedIn ? (
                      <Color green key={gen.name}>
                        ✓ {gen.name}
                      </Color>
                    ) : (
                      <Color key={gen.name}>
                        {'  '}
                        {gen.name}
                      </Color>
                    ),
                  )}
                </Box>
                <Box flexDirection="column" marginLeft={4}>
                  {generators.map(gen =>
                    gen.generating ? (
                      <Color dim key={gen.name}>
                        Generating...
                      </Color>
                    ) : gen.generatedIn ? (
                      <Color dim key={gen.name}>
                        Generated in {formatms(gen.generatedIn)}
                      </Color>
                    ) : (
                      <Color dim key={gen.name}>
                        Waiting for migrations...
                      </Color>
                    ),
                  )}
                </Box>
              </>
            )}
          </Box>
        </Box>
        <Box flexDirection="column" marginLeft={2} marginTop={0}>
          {warnings && warnings.length > 0 && onSubmitWarningsPrompt ? (
            <WarningsPrompt warnings={warnings} onSubmitWarningsPrompt={onSubmitWarningsPrompt} />
          ) : (
            <>
              <Box marginTop={1} width={this.width} justifyContent="space-between">
                <Box>
                  <Color bold>Studio endpoint: </Color>
                  {supportsHyperlinks.stdout ? (
                    <Link url={'http://localhost:' + this.props.studioPort}>
                      http://localhost:{this.props.studioPort}/
                    </Link>
                  ) : (
                    <Color underline>http://localhost:{this.props.studioPort}/</Color>
                  )}
                </Box>
              </Box>
              <Color dim>{'─'.repeat(this.width)}</Color>
              <Box>
                <Box>
                  <Color bold>d: </Color>
                  <Color dim>diff</Color>
                </Box>
              </Box>
            </>
          )}
        </Box>
      </Box>
    )
  }
}

export const DevInkComponent = (props: DevComponentProps) => (
  <StdinContext.Consumer>
    {({ stdin, setRawMode }) => (
      <StdoutContext.Consumer>
        {({ stdout }) => <DevComponent stdout={stdout} stdin={stdin} setRawMode={setRawMode} {...props} />}
      </StdoutContext.Consumer>
    )}
  </StdinContext.Consumer>
)


"test:eslint": "../../node_modules/.bin/eslint --ext .ts,.tsx ./src",

keep keep keep keep keep replace keep keep keep keep

@tanstack/query-async-storage-persister

TODO

../../node_modules/.bin/tsc -p tsconfig.json --noEmit --emitDeclarationOnly false


import * as React from 'react'

import { hydrate, HydrateOptions } from '@tanstack/query-core'
import { useQueryClient } from './QueryClientProvider'
import { ContextOptions } from './types'

export function useHydrate(
  state: unknown,
  options: HydrateOptions & ContextOptions = {},
) {
  const queryClient = useQueryClient({ context: options.context })

  const optionsRef = React.useRef(options)
  optionsRef.current = options

  // Running hydrate again with the same queries is safe,
  // it wont overwrite or initialize existing queries,
  // relying on useMemo here is only a performance optimization.
  // hydrate can and should be run *during* render here for SSR to work properly
  React.useMemo(() => {
    if (state) {
      hydrate(queryClient, state, optionsRef.current)
    }
  }, [queryClient, state])
}

export interface HydrateProps {
  state?: unknown
  options?: HydrateOptions
  children?: React.ReactNode
}

export const Hydrate = ({ children, options, state }: HydrateProps) => {
  useHydrate(state, options)
  return children as React.ReactElement
}


name: react-query tests

on:
  push:
    branches:
      - '1.x'
      - '2.x'
      - '3.x'
jobs:
  test:
    name: 'Node ${{ matrix.node }}, React ${{ matrix.react }}'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [14, 16]
        react: [17, 18]
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node }}
      - name: Install dependencies
        uses: bahmutov/npm-install@v1
      - run: yarn test:ci
        env:
          REACTJS_VERSION: ${{ matrix.react }}
      - run: yarn test:size
        if: matrix.node == '16' && matrix.react == '18'
        env:
          BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v2

  publish-module:
    name: 'Publish Module to NPM'
    needs: test
    # publish only when merged in master on original repo, not on PR
    if: github.repository == 'TanStack/query' && (github.ref == 'refs/heads/1.x' || github.ref == 'refs/heads/2.x' || github.ref == 'refs/heads/3.x')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: 14
          registry-url: https://registry.npmjs.org/
      - name: Install dependencies
        uses: bahmutov/npm-install@v1
      - run: yarn build
      - run: npx semantic-release@17
        env:
          NODE_AUTH_TOKEN: ${{secrets.npm_token}}
          GH_TOKEN: ${{secrets.GH_TOKEN}}


export default async function(...args) {
  const res = await fetch(...args);
  return await res.json();
}


"compile": "../../node_modules/.bin/tsc -p tsconfig.json --noEmit --emitDeclarationOnly false"

keep keep keep keep replace keep keep keep keep keep

@tanstack/query-core

TODO