SCALE — Build Lab
開発パターン · CLOUDFLARE FUNCTION

スナップショット API

CATEGORY開発パターン TYPECloudflare Function EFFORT120〜300分 DIFFICULTY
PRIMARY CODE
ts · scale-crm:functions/api/snapshot.ts
// POST /api/snapshot          → 手動スナップショット作成
// GET  /api/snapshot          → 一覧
// GET  /api/snapshot?id=X     → 特定スナップショット取得
import { corsResponse, corsError, handleOptions } from '../_lib/cors';

interface Env { DB: D1Database }

export const onRequestOptions = () => handleOptions();

export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
  try {
    const body = await request.json().catch(() => ({})) as any;
    const reason = body.reason || 'manual';
    const user = (() => {
      const raw = request.headers.get('X-User') || 'unknown';
      try { return decodeURIComponent(raw); } catch { return raw; }
    })();
    const now = new Date().toISOString();

    // 全 app_data をJSON化
    const { results } = await env.DB.prepare('SELECT key, value FROM app_data').all();
    const payload = JSON.stringify({
      created_at: now,
      reason,
      user,
      data: results,
    });

    const id = 'snap_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
    await env.DB.prepare(
      'INSERT INTO snapshots (id, taken_at, taken_by, reason, payload) VALUES (?, ?, ?, ?, ?)'
    ).bind(id, now, user, reason, payload).run();

    // 古いスナップショット削除(最新100個保持)
    await env.DB.prepare(
      "DELETE FROM snapshots WHERE id NOT IN (SELECT id FROM snapshots ORDER BY taken_at DESC LIMIT 100)"
    ).run();

    return corsResponse({ ok: true, id, taken_at: now, count: results.length });
  } catch (e: any) {
    return corsError(e.message);
  }
};

export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {
  try {
    const url = new URL(request.url);
    const id = url.searchParams.get('id');
    if (id) {
      const row = await env.DB.prepare(
        'SELECT id, taken_at, taken_by, reason, payload FROM snapshots WHERE id = ?'
      ).bind(id).first();
      if (!row) return corsResponse({ ok: false, error: 'not found' }, 404);
      return corsResponse({ ok: true, snapshot: row });
    }
    const { results } = await env.DB.prepare(
      'SELECT id, taken_at, taken_by, reason FROM snapshots ORDER BY taken_at DESC LIMIT 100'
    ).all();
    return corsResponse({ ok: true, count: results.length, snapshots: results });
  } catch (e: any) {
    return corsError(e.message);
  }
};

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • データ事故対策
  • バージョン管理

スナップショット API

:LiTarget: 用途

全データのスナップショットを定期保存・復元する API。世代管理つき。

:LiSparkle: 特徴

  • 定期スナップショット
  • 世代管理
  • 復元 API
  • JSON ダンプ

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: scale-crm:functions/api/snapshot.ts の中身そのもの。コピペ即可。

// POST /api/snapshot          → 手動スナップショット作成
// GET  /api/snapshot          → 一覧
// GET  /api/snapshot?id=X     → 特定スナップショット取得
import { corsResponse, corsError, handleOptions } from '../_lib/cors';

interface Env { DB: D1Database }

export const onRequestOptions = () => handleOptions();

export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
  try {
    const body = await request.json().catch(() => ({})) as any;
    const reason = body.reason || 'manual';
    const user = (() => {
      const raw = request.headers.get('X-User') || 'unknown';
      try { return decodeURIComponent(raw); } catch { return raw; }
    })();
    const now = new Date().toISOString();

    // 全 app_data をJSON化
    const { results } = await env.DB.prepare('SELECT key, value FROM app_data').all();
    const payload = JSON.stringify({
      created_at: now,
      reason,
      user,
      data: results,
    });

    const id = 'snap_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
    await env.DB.prepare(
      'INSERT INTO snapshots (id, taken_at, taken_by, reason, payload) VALUES (?, ?, ?, ?, ?)'
    ).bind(id, now, user, reason, payload).run();

    // 古いスナップショット削除(最新100個保持)
    await env.DB.prepare(
      "DELETE FROM snapshots WHERE id NOT IN (SELECT id FROM snapshots ORDER BY taken_at DESC LIMIT 100)"
    ).run();

    return corsResponse({ ok: true, id, taken_at: now, count: results.length });
  } catch (e: any) {
    return corsError(e.message);
  }
};

export const onRequestGet: PagesFunction<Env> = async ({ request, env }) => {
  try {
    const url = new URL(request.url);
    const id = url.searchParams.get('id');
    if (id) {
      const row = await env.DB.prepare(
        'SELECT id, taken_at, taken_by, reason, payload FROM snapshots WHERE id = ?'
      ).bind(id).first();
      if (!row) return corsResponse({ ok: false, error: 'not found' }, 404);
      return corsResponse({ ok: true, snapshot: row });
    }
    const { results } = await env.DB.prepare(
      'SELECT id, taken_at, taken_by, reason FROM snapshots ORDER BY taken_at DESC LIMIT 100'
    ).all();
    return corsResponse({ ok: true, count: results.length, snapshots: results });
  } catch (e: any) {
    return corsError(e.message);
  }
};

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/株式会社SCALE/scale-lead/functions/api/snapshot.ts

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加