mcpField
E
electric-new-feature

electric-new-feature

github.com/electric-sql開発10.4k40 回インストール3 週間前に更新
リポジトリ

electric-new-feature について

Electric の機能をエンドツーエンドで実装する開発者向けスキルです。Postgres のテーブルからプロキシルート、コレクション、ライブクエリまでを扱い、よくある実装上の問題も説明します。

主な機能

  1. 01Electric のローカル環境構築
  2. 02Postgres テーブルとプロキシルートの作成
  3. 03スキーマとコレクションの定義
  4. 04ライブクエリと楽観的ミューテーションの構築
  5. 05同期実装でよくある誤りの回避

活用シーン

  1. 01同期対応の ToDo 機能の構築
  2. 02Electric データ用プロキシルートの追加
  3. 03TanStack DB の型や書き込み経路の問題調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/electric-sql/electric --skill electric-new-feature

インストール後、Claude Code で/electric-new-featureを入力して読み込まれたか確認します。

詳細

このスキルは、electric-shapes、electric-proxy-auth、electric-schema-shapesを基盤としています。まず、これらをお読みください。

Electric — 新機能のエンドツーエンド実装

セットアップ

0. Electricをローカルで起動する
# docker-compose.yml
services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: electric
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - '54321:5432'
    tmpfs:
      - /tmp
    command:
      - -c
      - listen_addresses=*
      - -c
      - wal_level=logical

  electric:
    image: electricsql/electric:latest
    environment:
      DATABASE_URL: postgresql://postgres:password@postgres:5432/electric?sslmode=disable
      ELECTRIC_INSECURE: true # Dev only — use ELECTRIC_SECRET in production
    ports:
      - '3000:3000'
    depends_on:
      - postgres
docker compose up -d
1. Postgresのテーブルを作成する
CREATE TABLE todos (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  text TEXT NOT NULL,
  completed BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE todos REPLICA IDENTITY FULL;
2. プロキシルートを作成する

プロキシはElectricプロトコルのパラメーターを転送し、サーバー側のシークレットを追加します。使用しているフレームワークのサーバールートのパターン(TanStack Start、Next.js APIルート、Expressなど)を使用してください。

// Example: TanStack Start — src/routes/api/todos.ts
import { createFileRoute } from '@tanstack/react-router'
import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from '@electric-sql/client'

const serve = async ({ request }: { request: Request }) => {
  const url = new URL(request.url)
  const electricUrl = process.env.ELECTRIC_URL || 'http://localhost:3000'
  const origin = new URL(`${electricUrl}/v1/shape`)

  url.searchParams.forEach((v, k) => {
    if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(k))
      origin.searchParams.set(k, v)
  })

  origin.searchParams.set('table', 'todos')

  // Add auth if using Electric Cloud
  if (process.env.ELECTRIC_SOURCE_ID && process.env.ELECTRIC_SECRET) {
    origin.searchParams.set('source_id', process.env.ELECTRIC_SOURCE_ID)
    origin.searchParams.set('secret', process.env.ELECTRIC_SECRET)
  }

  const res = await fetch(origin)
  const headers = new Headers(res.headers)
  headers.delete('content-encoding')
  headers.delete('content-length')
  return new Response(res.body, {
    status: res.status,
    statusText: res.statusText,
    headers,
  })
}

export const Route = createFileRoute('/api/todos')({
  server: {
    handlers: {
      GET: serve,
    },
  },
})
3. スキーマを定義する
// db/schema.ts — Zod schema matching your Postgres table
import { z } from 'zod'

export const todoSchema = z.object({
  id: z.string().uuid(),
  user_id: z.string().uuid(),
  text: z.string(),
  completed: z.boolean(),
  created_at: z.date(),
})

export type Todo = z.infer<typeof todoSchema>

Drizzleを使用する場合は、drizzle-zodのcreateSelectSchema(todosTable)を使って、テーブル定義からスキーマを生成してください。

4. ミューテーションエンドポイントを作成する

フレームワークのサーバー関数またはAPIルートを使って、書き込みエンドポイントを実装してください。エンドポイントは、ミューテーションと同じトランザクションから{ txid }を返す必要があります。

// Example: server function that inserts and returns txid
async function createTodo(todo: { text: string; user_id: string }) {
  const client = await pool.connect()
  try {
    await client.query('BEGIN')
    const result = await client.query(
      'INSERT INTO todos (text, user_id) VALUES ($1, $2) RETURNING id',
      [todo.text, todo.user_id]
    )
    const txResult = await client.query(
      'SELECT pg_current_xact_id()::xid::text AS txid'
    )
    await client.query('COMMIT')
    return { id: result.rows[0].id, txid: Number(txResult.rows[0].txid) }
  } finally {
    client.release()
  }
}
5. TanStack DBコレクションを作成する
import { createCollection } from '@tanstack/react-db'
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
import { todoSchema } from './db/schema'

export const todoCollection = createCollection(
  electricCollectionOptions({
    id: 'todos',
    schema: todoSchema,
    getKey: (row) => row.id,
    shapeOptions: {
      url: new URL(
        '/api/todos',
        typeof window !== 'undefined'
          ? window.location.origin
          : 'http://localhost:5173'
      ).toString(),
      // Electric auto-parses: bool, int2, int4, float4, float8, json, jsonb
      // You only need custom parsers for types like timestamptz, date, numeric
      // See electric-shapes/references/type-parsers.md for the full list
      parser: {
        timestamptz: (date: string) => new Date(date),
      },
    },
    onInsert: async ({ transaction }) => {
      const { modified: newTodo } = transaction.mutations[0]
      const result = await createTodo({
        text: newTodo.text,
        user_id: newTodo.user_id,
      })
      return { txid: result.txid }
    },
    onUpdate: async ({ transaction }) => {
      const { modified: updated } = transaction.mutations[0]
      const result = await updateTodo(updated.id, {
        text: updated.text,
        completed: updated.completed,
      })
      return { txid: result.txid }
    },
    onDelete: async ({ transaction }) => {
      const { original: deleted } = transaction.mutations[0]
      const result = await deleteTodo(deleted.id)
      return { txid: result.txid }
    },
  })
)
6. ライブクエリを構築する
import { useLiveQuery, eq } from '@tanstack/react-db'

export function TodoList() {
  const { data: todos } = useLiveQuery((q) =>
    q
      .from({ todo: todoCollection })
      .where(({ todo }) => eq(todo.completed, false))
      .orderBy(({ todo }) => todo.created_at, 'desc')
      .limit(50)
  )

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}
7. 楽観的ミューテーション
const handleAdd = () => {
  todoCollection.insert({
    id: crypto.randomUUID(),
    text: 'New todo',
    completed: false,
    created_at: new Date(),
  })
}

const handleToggle = (todo) => {
  todoCollection.update(todo.id, (draft) => {
    draft.completed = !draft.completed
  })
}

const handleDelete = (todoId) => todoCollection.delete(todoId)

よくある間違い

HIGH TanStack DBのスキーマが型を処理するため、パーサーを削除する

誤り:

// "My Zod schema has z.coerce.date() so I don't need a parser"
electricCollectionOptions({
  schema: z.object({ created_at: z.coerce.date() }),
  shapeOptions: { url: '/api/todos' }, // No parser!
})

正しい例:

electricCollectionOptions({
  schema: z.object({ created_at: z.coerce.date() }),
  shapeOptions: {
    url: '/api/todos',
    parser: { timestamptz: (date: string) => new Date(date) },
  },
})

Electricの同期経路では、データがTanStack DBのスキーマを経由せず、コレクションストアに直接渡されます。shapeOptionsのparserは同期経路での型変換を処理し、スキーマはミューテーション経路を処理します。両方が必要です。パーサーがないと、timestamptzは文字列として渡されるため、getTime()などのDateメソッドは実行時に失敗します。

CRITICAL 古いelectrify()双方向同期APIを使用する

誤り:

const { db } = await electrify(conn, schema)
await db.todos.create({ text: 'New todo' })

正しい例:

todoCollection.insert({ id: crypto.randomUUID(), text: 'New todo' })
// Write path: collection.insert() → onInsert → API → Postgres → txid → awaitTxId

旧ElectricSQL(v0.x)には、双方向のSQLite同期がありました。現在のElectricは読み取り専用です。書き込みはAPIエンドポイント経由で行い、txidハンドシェイクを通じて調整します。

出典:AGENTS.md:386-392

HIGH パスベースのテーブルURLパターンを使用する

誤り:

const stream = new ShapeStream({
  url: 'http://localhost:3000/v1/shape/todos?offset=-1',
})

正しい例:

const stream = new ShapeStream({
  url: 'http://localhost:3000/v1/shape?table=todos&offset=-1',
})

テーブルをパスセグメントとして指定するパターン(/v1/shape/todos)は、v0.8.0で削除されました。テーブルはクエリパラメーターで指定するようになりました。

出典:packages/sync-service/CHANGELOG.md:1124

MEDIUM handleの代わりにshape_idを使用する

誤り:

const stream = new ShapeStream({
  url: '/api/todos',
  params: { shape_id: '12345' },
})

正しい例:

const stream = new ShapeStream({
  url: '/api/todos',
  handle: '12345',
})

v0.8.0でshape_idからhandleに名称が変更されました。

出典:packages/sync-service/CHANGELOG.md:1123

関連項目:electric-orm/SKILL.md — ORMトランザクションからtxidを取得する方法。 関連項目:electric-proxy-auth/SKILL.md — エンドツーエンドの機能実装手順には、プロキシルートの設定が含まれています。

バージョン

@electric-sql/client v1.5.10、@tanstack/react-dbの最新バージョンを対象としています。

コメント