mcpField
E
electric-new-feature

electric-new-feature

github.com/electric-sql开发10.4k40 次安装3 周前更新
代码仓库

关于 electric-new-feature

指导开发者端到端构建 Electric 新功能的技能,涵盖 Postgres 表、代理路由、数据集合与实时查询,并说明常见实现陷阱。

主要功能

  1. 01搭建本地 Electric 环境
  2. 02创建 Postgres 表与代理路由
  3. 03定义数据模式并创建集合
  4. 04构建实时查询与乐观变更
  5. 05说明同步实现中的常见错误

适用场景

  1. 01从零构建待办事项同步功能
  2. 02为 Electric 数据流接入代理路由
  3. 03排查 TanStack DB 与 Electric 的类型或写入问题

安装

复制下面的命令,在终端中运行。

$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 route、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. 定义 schema
// 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) 从表定义生成 schema。

4. 创建 mutation 端点

使用你所用框架的服务端函数或 API route 实现写入端点。该端点必须从执行 mutation 的同一事务中返回 { 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. 乐观 mutation
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)

常见错误

高优先级:因 TanStack DB schema 会处理类型而移除解析器

错误:

// "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 schema。shapeOptions 中的 parser 负责在同步路径上进行类型强制转换;schema 则处理 mutation 路径。两者都不可缺少。如果没有 parser,timestamptz 会以字符串形式传入,导致 getTime() 或其他 Date 方法在运行时失败。

严重:使用旧版 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

高优先级:使用基于路径的表 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

中优先级:使用 shape_id 而不是 handle

错误:

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。

另请参阅:electric-orm/SKILL.md — 从 ORM 事务中获取 txid。 另请参阅:electric-proxy-auth/SKILL.md — 端到端功能流程包含设置代理路由。

版本

目标版本:@electric-sql/client v1.5.10、@tanstack/react-db 最新版。

评论

支持 Markdown