Open5
MySQLのデータをTypeScriptファイルに抽出する
前提
API側でリソースくっつけて返却するのではなく、フロントエンドやBFFがその処理を行う場合、複数のAPIをくっつけて画面表示する必要がある
モチベーション
フロントエンドのテストを書くたびに、APIが返却するモックデータを考えるのを楽にしたい
イメージ
- サーバーは全部mswでモックする
- テスト毎にサーバーレスポンスを変えたいケースはいくらでもあるので、そこは大人しく手動で作成する
- seedデータを流した後にAPIが返却する値と、mswが返却する値が同じになれば良さそう
- テスト用のmockを少しいじると色々壊れないようにしたい
- mswの返却値を直接書くのではなく、mock dbからimportした値にする
- dbの値が変わった場合は、フロントエンドのテストデータも一緒に変わって、落ちるべくテストが落ちるのが良いはず
- 修正むずそうだが、ここは落ちるほうが望ましいはず
これらができればテスト以外でもモックサーバーが使えるようにもなるはず
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)
})
リレーションをどうするか
-
user
テーブルのidを他のテーブルが使うようなケースがいっぱいある - 該当するdbからimoprtした値を入れるのがよさそう。もしくは定数用意してそれをimportする
頑張らなくてもよいのでは?
- dbやseedデータが変わるたびにスクリプト再実行して作り直すとすると、mockの戻り値にmock dbの値をimportすればデータ不整合はおきないはず
- 基本のmswレスポンスはこれで問題ない
- テスト毎に基本のレスポンスでない値変える場合は、どうせdbやapiのこと考えながら然るべきデータを入れないといけないはず。そうなると、画面からPOSTやDELETEしてDB書き換え、GETを叩いてAPIのレスポンスをそのままmockの値に入れたくなるので、変に変数化するより楽に出来そう。基本のレスポンスがまんまseedとおなじになる前提があるので、他のテストに影響しない。
型定義どうするか
- Prismaの定義
- MySQLの定義
- 出力した変数から定義
- Prismaの定義からシンプルなTSの型に定義するのが一番正確なはず
- 他はMySQLの定義から生成、さらにはMySQLからTSに変換した変数から出力する
そもそも必要か?
- 出力したTSの推論で問題なさそうだからなくても大丈夫
型推論した値を型定義として宣言する方法
- 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()