Newer
Older
supabase-sample / src / app / api / ingest-csv / route.ts
Shinya Tomozumi on 31 May 5 KB テーブル情報の更新
import { NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { parse } from 'csv-parse';
import fs from 'fs/promises';
import path from 'path';

// Supabaseクライアントの初期化 (前回のものと同じ)
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY;
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; // 必要に応じてService Role Keyを使用

if (!supabaseUrl || (!supabaseAnonKey && !supabaseServiceRoleKey)) {
  throw new Error('Supabase URL or Anon/Service Role Key is missing in environment variables.');
}

// サーバーサイド用のSupabaseクライアントを作成
// 本番環境では anonKey の代わりに serviceRoleKey を使うことを強く推奨します。
// 例: const supabase = createClient(supabaseUrl, supabaseServiceRoleKey || supabaseAnonKey, { ... });
const supabase = createClient(supabaseUrl, supabaseAnonKey as string, { // デバッグ用に anonKey を使用
  auth: {
    persistSession: false,
  },
});

// CSVファイルをパースしてデータを取得するヘルパー関数 (前回のものと同じ)
async function parseCsvFile(filePath: string): Promise<any[]> {
  const fileContent = await fs.readFile(filePath, { encoding: 'utf8' });

  return new Promise((resolve, reject) => {
    parse(
      fileContent,
      {
        columns: true, // ヘッダー行をカラム名として使用
        skip_empty_lines: true,
      },
      (err, records) => {
        if (err) {
          return reject(err);
        }
        resolve(records);
      }
    );
  });
}

export async function GET(request: Request) {
  const results: {
    tableName: string;
    status: string;
    insertedCount?: number;
    error?: string;
    details?: any;
    hint?: any;
    code?: any
  }[] = [];

  try {
    const dataDirPath = path.join(process.cwd(), 'data');
    let files: string[];

    try {
      files = await fs.readdir(dataDirPath);
    } catch (readDirError: any) {
      if (readDirError.code === 'ENOENT') {
        return NextResponse.json(
          { message: 'Data directory not found. Please create a "data" folder at the project root.', status: 'error' },
          { status: 404 }
        );
      }
      console.error('Error reading data directory:', readDirError);
      return NextResponse.json(
        { message: `Failed to read data directory: ${readDirError.message}`, status: 'error' },
        { status: 500 }
      );
    }

    const csvFiles = files.filter(file => file.endsWith('.csv'));

    if (csvFiles.length === 0) {
      return NextResponse.json(
        { message: 'No CSV files found in the "data" directory.', status: 'success', results: [] },
        { status: 200 }
      );
    }

    for (const csvFileName of csvFiles) {
      const tableName = path.parse(csvFileName).name.toLowerCase(); // ファイル名からテーブル名を決定 (小文字に変換)
      const csvFilePath = path.join(dataDirPath, csvFileName);

      console.log(`Processing ${csvFileName} for table ${tableName}...`);

      try {
        let records = await parseCsvFile(csvFilePath);

        if (records.length === 0) {
          console.log(`CSV file ${csvFileName} is empty. Skipping insertion.`);
          results.push({ tableName, status: 'skipped', error: 'CSV file is empty' });
          continue; // 次のファイルへ
        }

        // ここでレコードを変換する必要がある場合があります。
        // 例: USER_IDが自動採番の場合、CSVからUSER_IDを取り除く
        if (tableName === 'users' && records[0].USER_ID !== undefined) {
             // CSVにUSER_IDがあるが、Supabaseで自動採番されている場合、
             // そのカラムを除外して挿入するための変換を行う
            records = records.map(({ USER_ID, ...rest }) => rest);
            console.log(`Removed USER_ID column for insertion into 'users' table.`);
        }
        // 他のテーブルで同様の変換が必要ならここに追加


        console.log(`Parsed ${records.length} records from ${csvFileName}.`);

        const { data: insertedData, error: insertError } = await supabase
          .from(tableName)
          .insert(records);

        if (insertError) {
          console.error(`Supabase insertion error for ${tableName}:`, insertError);
          // ここを修正: insertError の詳細を results に含める
          results.push({
            tableName,
            status: 'failed',
            error: insertError.message || 'Unknown error', // エラーメッセージ
            details: (insertError as any).details || null, // Supabaseが返す詳細情報
            hint: (insertError as any).hint || null,     // Supabaseが返すヒント
            code: (insertError as any).code || null,     // Supabaseが返すエラーコード (PostgreSQLのコードなど)
          });
        } else {
          console.log(`Successfully inserted ${records.length} records into ${tableName} table.`);
          results.push({ tableName, status: 'success', insertedCount: records.length });
        }
      } catch (fileProcessError: any) {
        console.error(`Error processing ${csvFileName}:`, fileProcessError);
        results.push({
          tableName,
          status: 'failed',
          error: `Error processing file: ${fileProcessError.message}`,
        });
      }
    }

    return NextResponse.json({
      message: 'CSV ingestion process completed.',
      status: 'completed',
      results: results,
    });

  } catch (error) {
    console.error('Unexpected error during batch CSV ingestion:', error);
    return NextResponse.json(
      { message: 'An unexpected error occurred during batch CSV ingestion', status: 'error', error: (error as Error).message },
      { status: 500 }
    );
  }
}