Open5

MySQLのデータをTypeScriptファイルに抽出する

astatsuyaastatsuya

前提

API側でリソースくっつけて返却するのではなく、フロントエンドやBFFがその処理を行う場合、複数のAPIをくっつけて画面表示する必要がある

モチベーション

フロントエンドのテストを書くたびに、APIが返却するモックデータを考えるのを楽にしたい

イメージ

  • サーバーは全部mswでモックする
  • テスト毎にサーバーレスポンスを変えたいケースはいくらでもあるので、そこは大人しく手動で作成する
  • seedデータを流した後にAPIが返却する値と、mswが返却する値が同じになれば良さそう
  • テスト用のmockを少しいじると色々壊れないようにしたい
    • mswの返却値を直接書くのではなく、mock dbからimportした値にする
  • dbの値が変わった場合は、フロントエンドのテストデータも一緒に変わって、落ちるべくテストが落ちるのが良いはず
    • 修正むずそうだが、ここは落ちるほうが望ましいはず

これらができればテスト以外でもモックサーバーが使えるようにもなるはず

astatsuyaastatsuya

MySQLの全テーブル全レコードをTypeScriptファイルに移す

  • Prismaからいい感じに出来るかもだが、MySQLから愚直にやるのが簡単で確実だった
    • 動かない部分はcopilotに聞きながらやったらすぐ出来た
import mysql from 'mysql2/promise'
import fs from 'fs'
import path from 'path'
import { exec } from 'child_process'

const MOCK_DB_DIR = path.join(__dirname, '../src/test-utils/mock/db')

const exportAllTables = async (databaseName: string) => {
  const connection = await mysql.createConnection({
    host: 'localhost',
    port: 3307,
    user: '',
    password: '',
    database: databaseName
  })

  const [tables]: [any[], any] = await connection.execute('SHOW TABLES')

  const tableNames = tables.map((row: any) => Object.values(row)[0])

  for (const tableName of tableNames) {
    try {
      const [rows]: [any[], any] = await connection.execute(
        `SELECT * FROM ${tableName}`
      )

      const data = JSON.stringify(rows)
      const tsContent = `export const ${tableName} = ${data};`

      const filePath = path.join(MOCK_DB_DIR, databaseName, `${tableName}.ts`)
      fs.writeFileSync(filePath, tsContent)

      console.log(`Data exported to ${filePath}`)
    } catch (error) {
      console.error(`Error exporting table ${tableName}:`, error)
    }
  }

  if (connection) {
    await connection.end()
  }
}

const databaseNames = ['db1', 'db2']

const exportAllDatabases = async () => {
  for (const database of databaseNames) {
    const dirPath = path.join(MOCK_DB_DIR, database)
    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath, { recursive: true })
    }
    await exportAllTables(database)
  }
  exec(`npx prettier --write ${MOCK_DB_DIR}`)
}

exportAllDatabases().catch(err => {
  console.error('Error exporting data:', err)
})

astatsuyaastatsuya

リレーションをどうするか

  • userテーブルのidを他のテーブルが使うようなケースがいっぱいある
  • 該当するdbからimoprtした値を入れるのがよさそう。もしくは定数用意してそれをimportする

頑張らなくてもよいのでは?

  • dbやseedデータが変わるたびにスクリプト再実行して作り直すとすると、mockの戻り値にmock dbの値をimportすればデータ不整合はおきないはず
    • 基本のmswレスポンスはこれで問題ない
    • テスト毎に基本のレスポンスでない値変える場合は、どうせdbやapiのこと考えながら然るべきデータを入れないといけないはず。そうなると、画面からPOSTやDELETEしてDB書き換え、GETを叩いてAPIのレスポンスをそのままmockの値に入れたくなるので、変に変数化するより楽に出来そう。基本のレスポンスがまんまseedとおなじになる前提があるので、他のテストに影響しない。
astatsuyaastatsuya

型定義どうするか

  1. Prismaの定義
  2. MySQLの定義
  3. 出力した変数から定義
  • Prismaの定義からシンプルなTSの型に定義するのが一番正確なはず
  • 他はMySQLの定義から生成、さらにはMySQLからTSに変換した変数から出力する

そもそも必要か?

  • 出力したTSの推論で問題なさそうだからなくても大丈夫
astatsuyaastatsuya

型推論した値を型定義として宣言する方法

  • vscode上で推論できているから頑張れば出来るはず

ts-morphでやってみる

  • できた
    • こちらもgithub coiplotがかなり助けてくれた。先月はいまいちだったけど、今月のアップデートでgpt 4oになったから?
  • テーブルごとに生成したおかげで、1ファイル1変数。ファイル名と変数名が一致している。全部配列になってる。など扱いやすい条件が出来ているので結構あっさり出来た
  • 今回いらないけど、他のケースでも、変数から型定義を作成するのは使えるシーン多いはず

元ファイル

// src/test-utils/mock/db/UserRole.ts
export const UserRole = [
  { id: 1, clientId: 1, uid: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx', roleId: 10 },
  { id: 2, clientId: 2, uid: 'yyyyyyyyyyyyyyyyyyyyyyyyyyyy', roleId: 100 }
]

変更後のファイル

type UserRole = { id: number; clientId: number; uid: string; roleId: number }

export const UserRole: UserRole[] = [
  { id: 1, clientId: 1, uid: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx', roleId: 10 },
  { id: 2, clientId: 2, uid: 'yyyyyyyyyyyyyyyyyyyyyyyyyyyy', roleId: 100 }
]

スクリプト

import { exec } from 'child_process'
import { Project, type SourceFile, ts } from 'ts-morph'

const project = new Project({
  tsConfigFilePath: 'tsconfig.json'
})
const typeChecker = project.getTypeChecker()

const addTypeDefenitionFromVariable = (sourceFile: SourceFile) => {
  const pathName = sourceFile.getBaseNameWithoutExtension()
  const _exportDeclaration = sourceFile.getExportedDeclarations().get(pathName)

  const exportDeclaration = _exportDeclaration?.[0]

  if (!exportDeclaration) {
    throw new Error('Export declaration not found')
  }

  const type = typeChecker.getTypeAtLocation(exportDeclaration)
  const elementType = type.getArrayElementType()

  if (!elementType) {
    throw new Error('Element type not found')
  }

  const typeDefinition = {
    name: pathName,
    type: elementType.getText()
  }
  sourceFile.insertTypeAlias(0, typeDefinition)

  const variableDeclaration = exportDeclaration.asKindOrThrow(
    ts.SyntaxKind.VariableDeclaration
  )
  variableDeclaration.setType(`${typeDefinition.name}[]`)
}

const executeAll = () => {
  const files = './src/test-utils/mock/db/**/*.ts'
  const sourceFiles = project.addSourceFilesAtPaths(files)
  sourceFiles.forEach(sourceFile => {
    addTypeDefenitionFromVariable(sourceFile)
    sourceFile.saveSync()
  })

  project.saveSync()
  exec(`npx prettier --write ${files}`)
}
executeAll()