0.1.1 • Published 5 months ago

trpc-electric-sql v0.1.1

Weekly downloads
-
License
MIT
Repository
-
Last release
5 months ago

trpc-electric-sql

tRPC integration for ElectricSQL: ElectricSQL-native RPC calls

For when you're building with ElectricSQL but you want to run code on a server.

See the monorepo README for a longer writeup on why this library exists.

Install

npm install trpc-electric-sql

Example app

https://github.com/KyleAMathews/trpc-crdt/tree/main/examples/electric-sql

Usage

This integration with ElectricSQL serializes tRPC calls via an electrified trpc_calls table.

Add this table definition to your Postgres instance:

CREATE TABLE IF NOT EXISTS trpc_calls (
    id UUID PRIMARY KEY NOT NULL,
    createdAt TIMESTAMPTZ NOT NULL,
    elapsedMs INTEGER,
    path TEXT NOT NULL,
    input TEXT,
    type TEXT NOT NULL,
    state TEXT NOT NULL,
    clientId TEXT NOT NULL,
    response TEXT
);

ALTER TABLE trpc_calls ENABLE ELECTRIC;

Once DDLX support lands, we'll be able to restrict updates to trpc_calls rows to the server and the original creator.

You can then setup your browser/server code similar to the following.

Browser

import { createTRPCProxyClient } from "@trpc/client"
import { link, createElectricRef } from "trpc-electric-sql/link"
import { Electric, schema } from "../src/generated/client"
import { ElectricDatabase, electrify } from "electric-sql/wa-sqlite"
import { genUUID, uniqueTabId } from "electric-sql/util"
import { authToken } from "../auth"

const electricRef = createElectricRef<Electric>()
const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    link({
      electricRef,
      clientId: genUUID(),
    }),
  ],
})

const init = async () => {
  const token = authToken()
  const config = {
    auth: {
      token: token,
    },
  }

  const { tabId } = uniqueTabId()
  const tabScopedDbName = `electric-${tabId}.db`

  const conn = await ElectricDatabase.init(tabScopedDbName, ``)
  const electric = await electrify(conn, schema, config)

  const [shape, usersShape] = await Promise.all([
    electric.db.trpc_calls.sync(),
    electric.db.users.sync(),
  ])
  await Promise.all([shape.synced, usersShape.synced])

  electricRef.value = electric
  setElectric(electric)

  // Tell server to create a new user.
  await trpc.userCreate.mutate({id: `1`, name: `Kyle Mathews`})

  // The new user, written by the server, is now available in the local sqlite db:
  const user = await electric.db.users.findUnique({where: { id: `1` }})
}

init()

Server

import { adapter } from "trpc-electric-sql/adapter"
import { initTRPC, TRPCError } from "@trpc/server"
import { z } from "zod"

const t = initTRPC.create()
const router = t.router
const publicProcedure = t.procedure

const appRouter = router({
  userCreate: publicProcedure
    .input(z.object({ name: z.string(), id: z.string() }))
    .mutation(async (opts) => {
      const {
        input,
        ctx: {
          transact,
          electric: { db },
        },
      } = opts
      const user = { ..input }

      // Writes in the transact function gets applied at same time the trpc call
      // is finished.
      transact(() => {
        db.users.create({
          data: user,
        })
      })
    })
})

async function setupTRPC() {
  const config = {
    auth: {
      token: authToken(),
    },
    url: process.env.ELECTRIC_URL,
  }

  // Create the better-sqlite3 database connection.
  const conn = new Database(`local-data.db`)
  conn.pragma(`journal_mode = WAL`)

  // Instantiate your electric client.
  const electric = await electrify(conn, schema, config)
  const { db } = electric
  const [shape, usersShape] = await Promise.all([
    electric.db.trpc_calls.sync(),
    electric.db.users.sync(),
  ])
  await Promise.all([shape.synced, usersShape.synced])

  adapter({
    context: { electric },
    appRouter,
  })
}

setupTRPC()
0.1.1

5 months ago

0.1.0

6 months ago

0.0.4

6 months ago

0.0.3

6 months ago

0.0.2

6 months ago

0.0.1

6 months ago