From ede71958793daf0aa2ace91a50881f18596d208f Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 11 Dec 2024 15:09:20 +0100 Subject: [PATCH 1/4] Make Electric URL, database ID, and token configurable --- examples/remix/app/db.ts | 15 ++++----------- examples/remix/app/routes/api.items.ts | 6 +++--- examples/remix/app/routes/shape-proxy.ts | 11 ++++++++++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/examples/remix/app/db.ts b/examples/remix/app/db.ts index 1571b1341c..53084f236e 100644 --- a/examples/remix/app/db.ts +++ b/examples/remix/app/db.ts @@ -1,14 +1,7 @@ import pgPkg from "pg" -const { Client } = pgPkg +const { Pool } = pgPkg -const db = new Client({ - host: `localhost`, - port: 54321, - password: `password`, - user: `postgres`, - database: `electric`, -}) +const connectionString = process.env.DATABASE_URL ?? `postgresql://postgres:password@localhost:54321/electric` +const pool = new Pool({ connectionString }) -db.connect() - -export { db } +export { pool } diff --git a/examples/remix/app/routes/api.items.ts b/examples/remix/app/routes/api.items.ts index 7c3485bee5..f4be8ba0ee 100644 --- a/examples/remix/app/routes/api.items.ts +++ b/examples/remix/app/routes/api.items.ts @@ -1,12 +1,12 @@ import nodePkg from "@remix-run/node" const { json } = nodePkg import type { ActionFunctionArgs } from "@remix-run/node" -import { db } from "../db" +import { pool } from "../db" export async function action({ request }: ActionFunctionArgs) { if (request.method === `POST`) { const body = await request.json() - const result = await db.query( + const result = await pool.query( `INSERT INTO items (id) VALUES ($1) RETURNING id;`, [body.uuid] @@ -15,7 +15,7 @@ export async function action({ request }: ActionFunctionArgs) { } if (request.method === `DELETE`) { - await db.query(`DELETE FROM items;`) + await pool.query(`DELETE FROM items;`) return `ok` } diff --git a/examples/remix/app/routes/shape-proxy.ts b/examples/remix/app/routes/shape-proxy.ts index aeea7f448c..4c8a0139dc 100644 --- a/examples/remix/app/routes/shape-proxy.ts +++ b/examples/remix/app/routes/shape-proxy.ts @@ -2,11 +2,20 @@ import type { LoaderFunctionArgs } from "@remix-run/node" export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url) - const originUrl = new URL(`http://localhost:3000/v1/shape`) + const baseUrl = process.env.ELECTRIC_URL ?? `http://localhost:3000` + const originUrl = new URL(`/v1/shape`, baseUrl) url.searchParams.forEach((value, key) => { originUrl.searchParams.set(key, value) }) + if (process.env.DATABASE_ID) { + originUrl.searchParams.set(`database_id`, process.env.DATABASE_ID) + } + + if (process.env.ELECTRIC_TOKEN) { + originUrl.searchParams.set(`token`, process.env.ELECTRIC_TOKEN) + } + // When proxying long-polling requests, content-encoding & content-length are added // erroneously (saying the body is gzipped when it's not) so we'll just remove // them to avoid content decoding errors in the browser. From 8abba19fafb2b8673a0b86212fb482071f676066 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 11 Dec 2024 15:10:33 +0100 Subject: [PATCH 2/4] Configure SST for cloud deployment --- examples/remix/package.json | 13 +++-- examples/remix/sst-env.d.ts | 18 ++++++ examples/remix/sst.config.ts | 110 +++++++++++++++++++++++++++++++++++ pnpm-lock.yaml | 21 +++++-- 4 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 examples/remix/sst-env.d.ts create mode 100644 examples/remix/sst.config.ts diff --git a/examples/remix/package.json b/examples/remix/package.json index 767d540a87..aaea4c1de8 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -6,13 +6,14 @@ "license": "Apache-2.0", "type": "module", "scripts": { - "backend:up": "PROJECT_NAME=remix-example pnpm -C ../../ run example-backend:up && pnpm db:migrate", "backend:down": "PROJECT_NAME=remix-example pnpm -C ../../ run example-backend:down", + "backend:up": "PROJECT_NAME=remix-example pnpm -C ../../ run example-backend:up && pnpm db:migrate", + "build": "remix vite:build", "db:migrate": "dotenv -e ../../.env.dev -- pnpm exec pg-migrations apply --directory ./db/migrations", - "dev": "vite", - "build": "vite build", - "stylecheck": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "dev": "remix vite:dev", "preview": "vite preview", + "start": "remix-serve ./build/server/index.js", + "stylecheck": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -26,13 +27,15 @@ "pg": "^8.12.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sst": "3.3.64", "uuid": "^10.0.0" }, "devDependencies": { "@databases/pg-migrations": "^5.0.3", + "@types/aws-lambda": "8.10.146", + "@types/pg": "^8.11.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@types/pg": "^8.11.6", "@types/uuid": "*", "@vitejs/plugin-react": "^4.3.1", "dotenv": "^16.4.5", diff --git a/examples/remix/sst-env.d.ts b/examples/remix/sst-env.d.ts new file mode 100644 index 0000000000..5f3d383ac1 --- /dev/null +++ b/examples/remix/sst-env.d.ts @@ -0,0 +1,18 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* deno-fmt-ignore-file */ +import "sst" +export {} +declare module "sst" { + export interface Resource { + "RemixExample": { + "name": string + "type": "sst.aws.Bucket" + } + "remix": { + "type": "sst.aws.Remix" + "url": string + } + } +} diff --git a/examples/remix/sst.config.ts b/examples/remix/sst.config.ts new file mode 100644 index 0000000000..693324d036 --- /dev/null +++ b/examples/remix/sst.config.ts @@ -0,0 +1,110 @@ +/// + +import { execSync } from "child_process" + +const isProduction = (stage: string) => stage.toLowerCase() === `production` + +export default $config({ + app(input) { + return { + name: "remix", + removal: input?.stage === "production" ? "retain" : "remove", + protect: ["production"].includes(input?.stage), + home: "aws", + providers: { + cloudflare: `5.42.0`, + aws: { version: `6.57.0`, region: `eu-west-1` }, + postgresql: "3.14.0", + }, + } + }, + async run() { + if (!process.env.ELECTRIC_API || !process.env.ELECTRIC_ADMIN_API) + throw new Error( + `Env variables ELECTRIC_API and ELECTRIC_ADMIN_API must be set` + ) + + if (!process.env.EXAMPLES_DATABASE_HOST || !process.env.EXAMPLES_DATABASE_PASSWORD) { + throw new Error( + `Env variables EXAMPLES_DATABASE_HOST and EXAMPLES_DATABASE_PASSWORD must be set` + ) + } + + const provider = new postgresql.Provider("neon", { + host: process.env.EXAMPLES_DATABASE_HOST, + database: `neondb`, + username: `neondb_owner`, + password: process.env.EXAMPLES_DATABASE_PASSWORD, + }) + + const dbName = isProduction($app.stage) ? `remix-production` : `remix-${$app.stage}` + const pg = new postgresql.Database(dbName, {}, { provider }) + + const pgUri = $interpolate`postgresql://${provider.username}:${provider.password}@${provider.host}/${pg.name}?sslmode=require` + const electricInfo = pgUri.apply((uri) => { + return addDatabaseToElectric(uri, `eu-west-1`) + }) + + const bucket = new sst.aws.Bucket("RemixExample"); + const staticSite = new sst.aws.Remix("remix", { + link: [bucket], + environment: { + ELECTRIC_URL: process.env.ELECTRIC_API!, + ELECTRIC_TOKEN: electricInfo.token, + DATABASE_ID: electricInfo.id, + }, + domain: { + name: `remix${isProduction($app.stage) ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, + dns: sst.cloudflare.dns(), + }, + }) + + pgUri.apply((uri) => applyMigrations(uri)) + + return { + pgUri, + databaseId: electricInfo.id, + token: electricInfo.token, + url: staticSite.url, + } + }, +}) + +function applyMigrations(uri: string) { + execSync(`pnpm exec pg-migrations apply --directory ./db/migrations`, { + env: { + ...process.env, + DATABASE_URL: uri, + }, + }) +} + +async function addDatabaseToElectric( + uri: string, + region: string +): Promise<{ + id: string + token: string +}> { + const adminApi = process.env.ELECTRIC_ADMIN_API + const url = new URL(`/v1/databases`, adminApi) + const result = await fetch(url, { + method: `PUT`, + headers: { "Content-Type": `application/json` }, + body: JSON.stringify({ + database_url: uri, + region, + }), + }) + if (!result.ok) { + throw new Error( + `Could not add database to Electric (${ + result.status + }): ${await result.text()}` + ) + } + return (await result.json()) as { + token: string + id: string + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75ba3c4ad2..10459702a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -732,7 +732,7 @@ importers: version: 0.3.4 tsup: specifier: ^8.0.1 - version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) tsx: specifier: ^4.19.1 version: 4.19.2 @@ -772,6 +772,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + sst: + specifier: 3.3.64 + version: 3.3.64(hono@4.6.13) uuid: specifier: ^10.0.0 version: 10.0.0 @@ -779,6 +782,9 @@ importers: '@databases/pg-migrations': specifier: ^5.0.3 version: 5.0.3(typescript@5.6.3) + '@types/aws-lambda': + specifier: 8.10.146 + version: 8.10.146 '@types/pg': specifier: ^8.11.6 version: 8.11.10 @@ -1185,7 +1191,7 @@ importers: version: 0.3.4 tsup: specifier: ^8.0.1 - version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) typescript: specifier: ^5.5.2 version: 5.6.3 @@ -1261,7 +1267,7 @@ importers: version: 0.3.4 tsup: specifier: ^8.0.1 - version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) typescript: specifier: ^5.5.2 version: 5.6.3 @@ -1321,7 +1327,7 @@ importers: version: 0.3.4 tsup: specifier: ^8.0.1 - version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) + version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0) typescript: specifier: ^5.5.2 version: 5.6.3 @@ -4452,6 +4458,9 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/aws-lambda@8.10.146': + resolution: {integrity: sha512-3BaDXYTh0e6UCJYL/jwV/3+GRslSc08toAiZSmleYtkAUyV5rtvdPYxrG/88uqvTuT6sb27WE9OS90ZNTIuQ0g==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -13408,6 +13417,8 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/aws-lambda@8.10.146': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.2 @@ -19067,7 +19078,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0): + tsup@8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0): dependencies: bundle-require: 5.0.0(esbuild@0.24.0) cac: 6.7.14 From bc5f8cdf2121999b28767f1ede68e8b2567afcba Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 11 Dec 2024 15:27:10 +0100 Subject: [PATCH 3/4] Fixed formatting --- examples/remix/app/db.ts | 4 +++- examples/remix/package.json | 1 + examples/remix/sst.config.ts | 28 +++++++++++++++++----------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/examples/remix/app/db.ts b/examples/remix/app/db.ts index 53084f236e..04101d77c4 100644 --- a/examples/remix/app/db.ts +++ b/examples/remix/app/db.ts @@ -1,7 +1,9 @@ import pgPkg from "pg" const { Pool } = pgPkg -const connectionString = process.env.DATABASE_URL ?? `postgresql://postgres:password@localhost:54321/electric` +const connectionString = + process.env.DATABASE_URL ?? + `postgresql://postgres:password@localhost:54321/electric` const pool = new Pool({ connectionString }) export { pool } diff --git a/examples/remix/package.json b/examples/remix/package.json index aaea4c1de8..795637ab0b 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -11,6 +11,7 @@ "build": "remix vite:build", "db:migrate": "dotenv -e ../../.env.dev -- pnpm exec pg-migrations apply --directory ./db/migrations", "dev": "remix vite:dev", + "format": "eslint . --fix", "preview": "vite preview", "start": "remix-serve ./build/server/index.js", "stylecheck": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", diff --git a/examples/remix/sst.config.ts b/examples/remix/sst.config.ts index 693324d036..77b3ee2faf 100644 --- a/examples/remix/sst.config.ts +++ b/examples/remix/sst.config.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import { execSync } from "child_process" @@ -7,14 +8,14 @@ const isProduction = (stage: string) => stage.toLowerCase() === `production` export default $config({ app(input) { return { - name: "remix", - removal: input?.stage === "production" ? "retain" : "remove", - protect: ["production"].includes(input?.stage), - home: "aws", + name: `remix`, + removal: input?.stage === `production` ? `retain` : `remove`, + protect: [`production`].includes(input?.stage), + home: `aws`, providers: { cloudflare: `5.42.0`, aws: { version: `6.57.0`, region: `eu-west-1` }, - postgresql: "3.14.0", + postgresql: `3.14.0`, }, } }, @@ -23,21 +24,26 @@ export default $config({ throw new Error( `Env variables ELECTRIC_API and ELECTRIC_ADMIN_API must be set` ) - - if (!process.env.EXAMPLES_DATABASE_HOST || !process.env.EXAMPLES_DATABASE_PASSWORD) { + + if ( + !process.env.EXAMPLES_DATABASE_HOST || + !process.env.EXAMPLES_DATABASE_PASSWORD + ) { throw new Error( `Env variables EXAMPLES_DATABASE_HOST and EXAMPLES_DATABASE_PASSWORD must be set` ) } - const provider = new postgresql.Provider("neon", { + const provider = new postgresql.Provider(`neon`, { host: process.env.EXAMPLES_DATABASE_HOST, database: `neondb`, username: `neondb_owner`, password: process.env.EXAMPLES_DATABASE_PASSWORD, }) - const dbName = isProduction($app.stage) ? `remix-production` : `remix-${$app.stage}` + const dbName = isProduction($app.stage) + ? `remix-production` + : `remix-${$app.stage}` const pg = new postgresql.Database(dbName, {}, { provider }) const pgUri = $interpolate`postgresql://${provider.username}:${provider.password}@${provider.host}/${pg.name}?sslmode=require` @@ -45,8 +51,8 @@ export default $config({ return addDatabaseToElectric(uri, `eu-west-1`) }) - const bucket = new sst.aws.Bucket("RemixExample"); - const staticSite = new sst.aws.Remix("remix", { + const bucket = new sst.aws.Bucket(`RemixExample`) + const staticSite = new sst.aws.Remix(`remix`, { link: [bucket], environment: { ELECTRIC_URL: process.env.ELECTRIC_API!, From edd9762192df8149a0ef54be8360218c08e55912 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Thu, 12 Dec 2024 11:14:24 +0100 Subject: [PATCH 4/4] Pass DB URL as env var to remix app --- examples/remix/sst.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/remix/sst.config.ts b/examples/remix/sst.config.ts index 77b3ee2faf..e4f7c36c1f 100644 --- a/examples/remix/sst.config.ts +++ b/examples/remix/sst.config.ts @@ -58,6 +58,7 @@ export default $config({ ELECTRIC_URL: process.env.ELECTRIC_API!, ELECTRIC_TOKEN: electricInfo.token, DATABASE_ID: electricInfo.id, + DATABASE_URL: pgUri, }, domain: { name: `remix${isProduction($app.stage) ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`,