From 4e9b9be4a5809576cc6bcb41207602fe43dfaef3 Mon Sep 17 00:00:00 2001 From: Stefanos Mousafeiris Date: Fri, 31 Jan 2025 02:22:47 +0200 Subject: [PATCH] feat: Shared SST code for example deployments (#2270) Co-authored-by: Valter Balegas --- .github/workflows/deploy_all_examples.yml | 6 +- .github/workflows/deploy_examples.yml | 14 + .../workflows/teardown_examples_pr_stack.yml | 23 +- .github/workflows/ts_test.yml | 1 + examples/.shared/.eslintrc.cjs | 35 +++ examples/.shared/.prettierrc | 6 + examples/.shared/lib/database.ts | 107 +++++++ examples/.shared/lib/infra.ts | 17 + examples/.shared/lib/neon.ts | 97 ++++++ examples/.shared/package.json | 26 ++ examples/.shared/sst.config.ts | 50 +++ examples/.shared/tsconfig.json | 28 ++ examples/linearlite-read-only/package.json | 2 +- examples/linearlite-read-only/sst.config.ts | 228 ++------------ examples/linearlite/package.json | 2 +- examples/nextjs/package.json | 2 +- examples/nextjs/sst.config.ts | 213 ++----------- examples/proxy-auth/package.json | 2 +- examples/proxy-auth/sst.config.ts | 188 +---------- examples/todo-app/package.json | 2 +- examples/todo-app/sst.config.ts | 284 ++++------------- examples/write-patterns/package.json | 2 +- examples/write-patterns/sst.config.ts | 296 ++++-------------- examples/yjs/package.json | 2 +- examples/yjs/sst-env.d.ts | 16 +- examples/yjs/sst.config.ts | 250 ++------------- package.json | 3 - pnpm-lock.yaml | 232 ++++++-------- pnpm-workspace.yaml | 1 + 29 files changed, 713 insertions(+), 1422 deletions(-) create mode 100644 examples/.shared/.eslintrc.cjs create mode 100644 examples/.shared/.prettierrc create mode 100644 examples/.shared/lib/database.ts create mode 100644 examples/.shared/lib/infra.ts create mode 100644 examples/.shared/lib/neon.ts create mode 100644 examples/.shared/package.json create mode 100644 examples/.shared/sst.config.ts create mode 100644 examples/.shared/tsconfig.json diff --git a/.github/workflows/deploy_all_examples.yml b/.github/workflows/deploy_all_examples.yml index 62c41e4126..2543618432 100644 --- a/.github/workflows/deploy_all_examples.yml +++ b/.github/workflows/deploy_all_examples.yml @@ -10,11 +10,13 @@ concurrency: jobs: deploy-examples: name: Deploy All Examples to Production - environment: 'Production' + environment: "Production" runs-on: ubuntu-latest env: - DEPLOY_ENV: 'production' + DEPLOY_ENV: "production" + SHARED_INFRA_VPC_ID: ${{ vars.SHARED_INFRA_VPC_ID }} + SHARED_INFRA_CLUSTER_ARN: ${{ vars.SHARED_INFRA_CLUSTER_ARN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/.github/workflows/deploy_examples.yml b/.github/workflows/deploy_examples.yml index e5b9c2c2c1..3c5416c118 100644 --- a/.github/workflows/deploy_examples.yml +++ b/.github/workflows/deploy_examples.yml @@ -17,6 +17,8 @@ jobs: env: DEPLOY_ENV: ${{ github.event_name == 'push' && 'production' || format('pr-{0}', github.event.number) }} + SHARED_INFRA_VPC_ID: ${{ vars.SHARED_INFRA_VPC_ID }} + SHARED_INFRA_CLUSTER_ARN: ${{ vars.SHARED_INFRA_CLUSTER_ARN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -53,6 +55,7 @@ jobs: working-directory: ./examples/yjs run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} + code=$! if [ -f ".sst/outputs.json" ]; then yjs=$(jq -r '.website' .sst/outputs.json) echo "yjs=$yjs" >> $GITHUB_ENV @@ -60,11 +63,13 @@ jobs: echo "sst outputs file not found. Exiting." exit 123 fi + exit $code - name: Deploy Linearlite Read Only working-directory: ./examples/linearlite-read-only run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} + code=$! if [ -f ".sst/outputs.json" ]; then linearlite_read_only=$(jq -r '.website' .sst/outputs.json) echo "linearlite_read_only=$linearlite_read_only" >> $GITHUB_ENV @@ -72,12 +77,14 @@ jobs: echo "sst outputs file not found. Exiting." exit 123 fi + exit $code - name: Deploy Write Patterns example working-directory: ./examples/write-patterns run: | pnpm --filter @electric-sql/client --filter @electric-sql/experimental --filter @electric-sql/react run build pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} + code=$! if [ -f ".sst/outputs.json" ]; then writes=$(jq -r '.website' .sst/outputs.json) echo "writes=$writes" >> $GITHUB_ENV @@ -85,11 +92,13 @@ jobs: echo "sst outputs file not found. Exiting." exit 123 fi + exit $code - name: Deploy NextJs example working-directory: ./examples/nextjs run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} + code=$! if [ -f ".sst/outputs.json" ]; then nextjs=$(jq -r '.website' .sst/outputs.json) echo "nextjs=$nextjs" >> $GITHUB_ENV @@ -97,11 +106,13 @@ jobs: echo "sst outputs file not found. Exiting." exit 123 fi + exit $code - name: Deploy TODO App example working-directory: ./examples/todo-app run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} + code=$! if [ -f ".sst/outputs.json" ]; then todoapp=$(jq -r '.website' .sst/outputs.json) echo "todoapp=$todoapp" >> $GITHUB_ENV @@ -109,11 +120,13 @@ jobs: echo "sst outputs file not found. Exiting." exit 123 fi + exit $code - name: Deploy proxy-auth example working-directory: ./examples/proxy-auth run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} + code=$! if [ -f ".sst/outputs.json" ]; then auth=$(jq -r '.website' .sst/outputs.json) echo "auth=$auth" >> $GITHUB_ENV @@ -121,6 +134,7 @@ jobs: echo "sst outputs file not found. Exiting." exit 123 fi + exit $code - name: Add comment to PR if: github.event_name == 'pull_request' diff --git a/.github/workflows/teardown_examples_pr_stack.yml b/.github/workflows/teardown_examples_pr_stack.yml index d99b89a32c..74591e833b 100644 --- a/.github/workflows/teardown_examples_pr_stack.yml +++ b/.github/workflows/teardown_examples_pr_stack.yml @@ -13,6 +13,18 @@ jobs: name: Teardown Examples PR stack environment: Pull request runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + example: + [ + "yjs", + "linearlite-read-only", + "write-patterns", + "nextjs", + "todo-app", + "proxy-auth", + ] env: DEPLOY_ENV: ${{ github.event_name == 'push' && 'production' || format('pr-{0}', github.event.number) }} @@ -45,15 +57,8 @@ jobs: restore-keys: | sst-cache-${{ runner.os }} - - name: Remove Linearlite - working-directory: examples/linearlite-read-only - run: | - export PR_NUMBER=${{ github.event.number }} - echo "Removing stage pr-$PR_NUMBER" - pnpm sst remove --stage "pr-$PR_NUMBER" - - - name: Remove NextJs example - working-directory: examples/nextjs + - name: Remove ${{ matrix.example }} example + working-directory: ./examples/${{ matrix.example }} run: | export PR_NUMBER=${{ github.event.number }} echo "Removing stage pr-$PR_NUMBER" diff --git a/.github/workflows/ts_test.yml b/.github/workflows/ts_test.yml index 1ba0feab87..139bd9778e 100644 --- a/.github/workflows/ts_test.yml +++ b/.github/workflows/ts_test.yml @@ -155,6 +155,7 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm -r --filter "$(jq '.name' -r package.json)^..." build + - run: pnpm --if-present run prepare - run: pnpm --if-present run typecheck - run: pnpm --if-present run build - run: pnpm --if-present run test diff --git a/examples/.shared/.eslintrc.cjs b/examples/.shared/.eslintrc.cjs new file mode 100644 index 0000000000..ae7270c83c --- /dev/null +++ b/examples/.shared/.eslintrc.cjs @@ -0,0 +1,35 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + `eslint:recommended`, + `plugin:@typescript-eslint/recommended`, + `plugin:prettier/recommended`, + ], + parserOptions: { + ecmaVersion: 2022, + requireConfigFile: false, + sourceType: `module`, + ecmaFeatures: { + jsx: true, + }, + }, + parser: `@typescript-eslint/parser`, + plugins: [`prettier`], + rules: { + quotes: [`error`, `backtick`], + 'no-unused-vars': `off`, + '@typescript-eslint/no-unused-vars': [ + `error`, + { + argsIgnorePattern: `^_`, + varsIgnorePattern: `^_`, + caughtErrorsIgnorePattern: `^_`, + }, + ], + }, + ignorePatterns: [`**/node_modules/**`, `.eslintrc.cjs`], +} diff --git a/examples/.shared/.prettierrc b/examples/.shared/.prettierrc new file mode 100644 index 0000000000..ad4d895523 --- /dev/null +++ b/examples/.shared/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/examples/.shared/lib/database.ts b/examples/.shared/lib/database.ts new file mode 100644 index 0000000000..a5c38e40ad --- /dev/null +++ b/examples/.shared/lib/database.ts @@ -0,0 +1,107 @@ +import { execSync } from 'node:child_process' +import { createNeonDb, getNeonConnectionString } from './neon' + +async function addDatabaseToElectric({ + dbUri, +}: { + dbUri: string +}): Promise<{ id: string; source_secret: string }> { + const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID + + if (!adminApi || !teamId) { + throw new Error(`ELECTRIC_ADMIN_API or ELECTRIC_TEAM_ID is not set`) + } + + const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID + const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET + if (!adminApiTokenId || !adminApiTokenSecret) { + throw new Error( + `ADMIN_API_TOKEN_CLIENT_ID or ADMIN_API_TOKEN_CLIENT_SECRET is not set` + ) + } + + const result = await fetch(`${adminApi}/v1/sources`, { + method: `PUT`, + headers: { + 'Content-Type': `application/json`, + 'CF-Access-Client-Id': adminApiTokenId, + 'CF-Access-Client-Secret': adminApiTokenSecret, + }, + body: JSON.stringify({ + database_url: dbUri, + region: `us-east-1`, + team_id: teamId, + }), + }) + + if (!result.ok) { + throw new Error( + `Could not add database to Electric (${result.status}): ${await result.text()}` + ) + } + + return await result.json() +} + +function applyMigrations( + dbUri: string, + migrationsDir: string = `./db/migrations` +) { + execSync(`pnpm exec pg-migrations apply --directory ${migrationsDir}`, { + env: { + ...process.env, + DATABASE_URL: dbUri, + }, + }) +} + +export function createDatabaseForCloudElectric({ + dbName, + migrationsDirectory, +}: { + dbName: string + migrationsDirectory: string +}) { + const neonProjectId = process.env.NEON_PROJECT_ID + if (!neonProjectId) { + throw new Error(`NEON_PROJECT_ID is not set`) + } + + const project = neon.getProjectOutput({ + id: neonProjectId, + }) + const { ownerName, dbName: resultingDbName } = createNeonDb({ + projectId: project.id, + branchId: project.defaultBranchId, + dbName, + }) + + const databaseUri = getNeonConnectionString({ + project, + roleName: ownerName, + databaseName: resultingDbName, + pooled: false, + }) + const pooledDatabaseUri = getNeonConnectionString({ + project, + roleName: ownerName, + databaseName: resultingDbName, + pooled: true, + }) + + if (migrationsDirectory) { + databaseUri.apply((uri) => applyMigrations(uri, migrationsDirectory)) + } + + const electricInfo = databaseUri.apply((dbUri) => + addDatabaseToElectric({ dbUri }) + ) + + return { + sourceId: electricInfo.id, + sourceSecret: electricInfo.source_secret, + databaseUri, + pooledDatabaseUri, + } +} diff --git a/examples/.shared/lib/infra.ts b/examples/.shared/lib/infra.ts new file mode 100644 index 0000000000..338d2321c0 --- /dev/null +++ b/examples/.shared/lib/infra.ts @@ -0,0 +1,17 @@ +export function getSharedCluster(serviceName: string): sst.aws.Cluster { + const sharedInfraVpcId = process.env.SHARED_INFRA_VPC_ID + const sharedInfraClusterArn = process.env.SHARED_INFRA_CLUSTER_ARN + if (!sharedInfraVpcId || !sharedInfraClusterArn) { + throw new Error( + `SHARED_INFRA_VPC_ID or SHARED_INFRA_CLUSTER_ARN is not set` + ) + } + + return sst.aws.Cluster.get(`${serviceName}-cluster`, { + id: sharedInfraClusterArn, + vpc: sst.aws.Vpc.get(`${serviceName}-vpc`, sharedInfraVpcId), + }) +} + +export const isProduction = () => + $app.stage.toLocaleLowerCase() === `production` diff --git a/examples/.shared/lib/neon.ts b/examples/.shared/lib/neon.ts new file mode 100644 index 0000000000..bfee72c17a --- /dev/null +++ b/examples/.shared/lib/neon.ts @@ -0,0 +1,97 @@ +export function getNeonConnectionString({ + project, + roleName, + databaseName, + pooled, +}: { + project: $util.Output + roleName: $util.Input + databaseName: $util.Input + pooled: boolean +}): $util.Output { + const passwordOutput = neon.getBranchRolePasswordOutput({ + projectId: project.id, + branchId: project.defaultBranchId, + roleName: roleName, + }) + + const endpoint = neon.getBranchEndpointsOutput({ + projectId: project.id, + branchId: project.defaultBranchId, + }) + const databaseHost = pooled + ? endpoint.endpoints?.apply((endpoints) => + endpoints![0].host.replace( + endpoints![0].id, + endpoints![0].id + `-pooler` + ) + ) + : project.databaseHost + return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` +} + +/** + * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with + * a Pulumi Command resource and `curl` to create and delete Neon databases. + */ +export function createNeonDb({ + projectId, + branchId, + dbName, +}: { + projectId: $util.Input + branchId: $util.Input + dbName: $util.Input +}): $util.Output<{ + dbName: string + ownerName: string +}> { + if (!process.env.NEON_API_KEY) { + throw new Error(`NEON_API_KEY is not set`) + } + + const ownerName = `neondb_owner` + + const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + -H 'Content-Type: application/json' \ + -d '{ + "database": { + "name": "'$DATABASE_NAME'", + "owner_name": "${ownerName}" + } + }' \ + && echo " SUCCESS" || echo " FAILURE"` + + const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` + + const deleteCommand = `curl -f -s -X 'DELETE' \ + "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ + -H 'Accept: application/json' \ + -H "Authorization: Bearer $NEON_API_KEY" \ + && echo " SUCCESS" || echo " FAILURE"` + + const result = new command.local.Command(`neon-db-command:${dbName}`, { + create: createCommand, + update: updateCommand, + delete: deleteCommand, + environment: { + NEON_API_KEY: process.env.NEON_API_KEY, + PROJECT_ID: projectId, + BRANCH_ID: branchId, + DATABASE_NAME: dbName, + }, + }) + return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { + if (stdout.endsWith(`SUCCESS`)) { + console.log(`Created Neon database ${dbName}`) + return { + dbName, + ownerName, + } + } else { + throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) + } + }) +} diff --git a/examples/.shared/package.json b/examples/.shared/package.json new file mode 100644 index 0000000000..715a627ee7 --- /dev/null +++ b/examples/.shared/package.json @@ -0,0 +1,26 @@ +{ + "name": "@electric-examples/shared", + "private": true, + "version": "0.0.1", + "author": "ElectricSQL", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "deploy": "sst deploy --stage shared", + "lint": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0", + "prepare": "sst install", + "stylecheck": "eslint . --quiet", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@databases/pg-migrations": "^5.0.3", + "@types/node": "^20.14.10", + "@types/pg": "^8.11.6", + "camelcase": "^8.0.0", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "pg": "^8.13.1", + "sst": "3.6.25", + "typescript": "^5.5.3" + } +} diff --git a/examples/.shared/sst.config.ts b/examples/.shared/sst.config.ts new file mode 100644 index 0000000000..b4b536ba98 --- /dev/null +++ b/examples/.shared/sst.config.ts @@ -0,0 +1,50 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +import camelcase from 'camelcase' + +export default $config({ + app() { + return { + name: `examples-infra`, + removal: `remove`, + home: `aws`, + providers: { + aws: { + version: `6.66.2`, + profile: process.env.CI ? undefined : `marketing`, + }, + neon: `0.6.3`, + command: `1.0.1`, + }, + } + }, + async run() { + const provider = new aws.Provider( + camelcase(`examples-infra-provider-${$app.stage}`), + { + region: `us-east-1`, + } + ) + const vpc = new sst.aws.Vpc( + camelcase(`examples-infra-vpc-${$app.stage}`), + {}, + { provider: provider } + ) + + const cluster = new sst.aws.Cluster( + camelcase(`examples-infra-cluster-${$app.stage}`), + { vpc }, + { provider } + ) + + // Set the following environment variables with the shared infra + // in GitHub CI environment + // SHARED_INFRA_VPC_ID + // SHARED_INFRA_CLUSTER_ARN + return { + sharedVpc: vpc.id, + sharedCluster: cluster.id, + } + }, +}) diff --git a/examples/.shared/tsconfig.json b/examples/.shared/tsconfig.json new file mode 100644 index 0000000000..84252e0881 --- /dev/null +++ b/examples/.shared/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2015", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/linearlite-read-only/package.json b/examples/linearlite-read-only/package.json index 701952e295..7e82ca8fd3 100644 --- a/examples/linearlite-read-only/package.json +++ b/examples/linearlite-read-only/package.json @@ -79,7 +79,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "fs-extra": "^10.0.0", "postcss": "^8.4.39", - "sst": "3.3.7", + "sst": "3.6.35", "tailwindcss": "^3.4.4", "typescript": "^5.5.3", "vite": "^4.4.5" diff --git a/examples/linearlite-read-only/sst.config.ts b/examples/linearlite-read-only/sst.config.ts index 94f5b92937..c1dbd8a093 100644 --- a/examples/linearlite-read-only/sst.config.ts +++ b/examples/linearlite-read-only/sst.config.ts @@ -2,15 +2,15 @@ /// import { execSync } from 'child_process' - -const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID -const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET +import { createDatabaseForCloudElectric } from '../.shared/lib/database' +import { isProduction } from '../.shared/lib/infra' export default $config({ app(input) { return { name: `linearlite-read-only`, - removal: input?.stage === `production` ? `retain` : `remove`, + removal: + input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`, home: `aws`, providers: { cloudflare: `5.42.0`, @@ -24,65 +24,42 @@ export default $config({ } }, async run() { - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_ID) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - const project = neon.getProjectOutput({ id: process.env.NEON_PROJECT_ID! }) - - const dbName = `linearlite-read-only-${$app.stage}` - - const { ownerName, dbName: resultingDbName } = createNeonDb({ - projectId: project.id, - branchId: project.defaultBranchId, - dbName, - }) - - const databaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: false, - }) - try { - databaseUri - .apply(async (dbUri) => { - applyMigrations(dbUri) - return dbUri + const dbName = `linearlite-read-only${isProduction() ? `` : `-stage-${$app.stage}`}` + + const { pooledDatabaseUri, sourceId, sourceSecret } = + createDatabaseForCloudElectric({ + dbName, + migrationsDirectory: `./db/migrations`, }) - .apply(loadData) - const electricInfo = databaseUri.apply((uri) => - addDatabaseToElectric(uri) - ) + pooledDatabaseUri.apply(loadData) + + const website = new sst.aws.StaticSite(`linearlite-read-only`, { + environment: { + VITE_ELECTRIC_URL: process.env.ELECTRIC_API!, + VITE_ELECTRIC_SOURCE_SECRET: sourceSecret, + VITE_ELECTRIC_SOURCE_ID: sourceId, + }, + build: { + command: `pnpm run --filter @electric-sql/client --filter @electric-sql/react --filter @electric-examples/linearlite-read-only build`, + output: `dist`, + }, + domain: { + name: `linearlite-read-only${$app.stage === `production` ? `` : `-stage-${$app.stage}`}.electric-sql.com`, + dns: sst.cloudflare.dns(), + }, + }) - const website = deployLinearLite(electricInfo) return { - databaseUri, - // source_id: electricInfo.id, - // source_secret: electricInfo.source_secret, website: website.url, } - } catch (e) { - console.error(`Failed to deploy linearlite-read-only stack`, e) + } catch (error) { + console.error(`Failed to deploy linearlite-read-only`, error) } }, }) -function applyMigrations(uri: string) { - execSync(`pnpm exec pg-migrations apply --directory ./db/migrations`, { - env: { - ...process.env, - DATABASE_URL: uri, - }, - }) -} - function loadData(uri: string) { execSync(`pnpm run db:load-data`, { env: { @@ -91,150 +68,3 @@ function loadData(uri: string) { }, }) } - -function deployLinearLite( - electricInfo: $util.Output<{ id: string; source_secret: string }> -) { - return new sst.aws.StaticSite(`linearlite-read-only`, { - environment: { - VITE_ELECTRIC_URL: process.env.ELECTRIC_API!, - VITE_ELECTRIC_SOURCE_SECRET: electricInfo.source_secret, - VITE_ELECTRIC_SOURCE_ID: electricInfo.id, - }, - build: { - command: `pnpm run --filter @electric-sql/client --filter @electric-sql/react --filter @electric-examples/linearlite-read-only build`, - output: `dist`, - }, - domain: { - name: `linearlite-read-only${$app.stage === `production` ? `` : `-stage-${$app.stage}`}.electric-sql.com`, - dns: sst.cloudflare.dns(), - }, - }) -} - -async function addDatabaseToElectric( - uri: string -): Promise<{ id: string; source_secret: string }> { - const adminApi = process.env.ELECTRIC_ADMIN_API - const teamId = process.env.ELECTRIC_TEAM_ID - - const result = await fetch(`${adminApi}/v1/sources`, { - method: `PUT`, - headers: { - 'Content-Type': `application/json`, - 'CF-Access-Client-Id': adminApiTokenId ?? ``, - 'CF-Access-Client-Secret': adminApiTokenSecret ?? ``, - }, - body: JSON.stringify({ - database_url: uri, - region: `us-east-1`, - team_id: teamId, - }), - }) - - if (!result.ok) { - throw new Error( - `Could not add database to Electric (${result.status}): ${await result.text()}` - ) - } - - return await result.json() -} - -function getNeonConnectionString({ - project, - roleName, - databaseName, - pooled, -}: { - project: $util.Output - roleName: $util.Input - databaseName: $util.Input - pooled: boolean -}): $util.Output { - const passwordOutput = neon.getBranchRolePasswordOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - roleName: roleName, - }) - - const endpoint = neon.getBranchEndpointsOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - }) - const databaseHost = pooled - ? endpoint.endpoints?.apply((endpoints) => - endpoints![0].host.replace( - endpoints![0].id, - endpoints![0].id + `-pooler` - ) - ) - : project.databaseHost - return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` -} - -/** - * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with - * a Pulumi Command resource and `curl` to create and delete Neon databases. - */ -function createNeonDb({ - projectId, - branchId, - dbName, -}: { - projectId: $util.Input - branchId: $util.Input - dbName: $util.Input -}): $util.Output<{ - dbName: string - ownerName: string -}> { - if (!process.env.NEON_API_KEY) { - throw new Error(`NEON_API_KEY is not set`) - } - - const ownerName = `neondb_owner` - - const createCommand = `curl -f -v "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - -H 'Content-Type: application/json' \ - -d '{ - "database": { - "name": "'$DATABASE_NAME'", - "owner_name": "${ownerName}" - } - }' 2>&1 \ - && echo " SUCCESS" || echo " FAILURE - Response: $?"` - - const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` - - const deleteCommand = `curl -f -v -X 'DELETE' \ - "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" 2>&1 \ - && echo " SUCCESS" || echo " FAILURE - Response: $?"` - - const result = new command.local.Command(`neon-db-command:${dbName}`, { - create: createCommand, - update: updateCommand, - delete: deleteCommand, - environment: { - NEON_API_KEY: process.env.NEON_API_KEY, - PROJECT_ID: projectId, - BRANCH_ID: branchId, - DATABASE_NAME: dbName, - }, - }) - return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { - if (stdout.endsWith(`SUCCESS`)) { - console.log(`Created Neon database ${dbName}`) - return { - dbName, - ownerName, - } - } else { - throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) - } - }) -} diff --git a/examples/linearlite/package.json b/examples/linearlite/package.json index 2cbb7d3ceb..ece6569e1d 100644 --- a/examples/linearlite/package.json +++ b/examples/linearlite/package.json @@ -94,7 +94,7 @@ "fs-extra": "^10.0.0", "globals": "^15.13.0", "postcss": "^8.4.39", - "sst": "3.3.7", + "sst": "3.6.35", "supabase": "^1.226.3", "tailwindcss": "^3.4.4", "tsx": "^4.19.1", diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index cd230b73bf..b877621076 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -35,7 +35,7 @@ "@vitejs/plugin-react": "^4.3.1", "dotenv": "^16.4.5", "eslint": "^8.57.0", - "sst": "3.3.7", + "sst": "3.6.35", "typescript": "^5.5.3", "vite": "^5.3.4" } diff --git a/examples/nextjs/sst.config.ts b/examples/nextjs/sst.config.ts index efe5f45222..0f28547cc6 100644 --- a/examples/nextjs/sst.config.ts +++ b/examples/nextjs/sst.config.ts @@ -1,7 +1,8 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import { execSync } from "child_process" +import { createDatabaseForCloudElectric } from "../.shared/lib/database" +import { isProduction } from "../.shared/lib/infra" export default $config({ app(input) { @@ -21,198 +22,28 @@ export default $config({ } }, async run() { - const project = neon.getProjectOutput({ id: process.env.NEON_PROJECT_ID! }) + const dbName = `nextjs${isProduction() ? `` : `-stage-${$app.stage}`}` - const dbName = `nextjs-${$app.stage}` - - const { ownerName, dbName: resultingDbName } = createNeonDb({ - projectId: project.id, - branchId: project.defaultBranchId, - dbName, - }) - - const pooledDatabaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: true, - }) - const databaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: false, + const { sourceId, sourceSecret, pooledDatabaseUri } = + createDatabaseForCloudElectric({ + dbName, + migrationsDirectory: `./db/migrations`, + }) + + const website = new sst.aws.Nextjs(`nextjs`, { + environment: { + ELECTRIC_URL: process.env.ELECTRIC_API!, + ELECTRIC_SOURCE_SECRET: sourceSecret, + ELECTRIC_SOURCE_ID: sourceId, + DATABASE_URL: pooledDatabaseUri, + }, + domain: { + name: `nextjs${$app.stage === `production` ? `` : `-stage-${$app.stage}`}.electric-sql.com`, + dns: sst.cloudflare.dns(), + }, }) - try { - databaseUri.apply(applyMigrations) - - const electricInfo = databaseUri.apply((uri) => - addDatabaseToElectric(uri) - ) - - const website = deployNextJsExample(electricInfo, pooledDatabaseUri) - return { - pooledDatabaseUri, - // source_id: electricInfo.id, - // source_secret: electricInfo.source_secret, - website: website.url, - } - } catch (e) { - console.error(`Failed to deploy nextjs example stack`, e) + return { + website: website.url, } }, }) - -function applyMigrations(uri: string) { - execSync(`pnpm exec pg-migrations apply --directory ./db/migrations`, { - env: { - ...process.env, - DATABASE_URL: uri, - }, - }) -} - -function deployNextJsExample( - electricInfo: $util.Output<{ id: string; source_secret: string }>, - uri: $util.Output -) { - return new sst.aws.Nextjs(`nextjs`, { - environment: { - ELECTRIC_URL: process.env.ELECTRIC_API!, - ELECTRIC_SOURCE_SECRET: electricInfo.source_secret, - ELECTRIC_SOURCE_ID: electricInfo.id, - DATABASE_URL: uri, - }, - domain: { - name: `nextjs${$app.stage === `production` ? `` : `-stage-${$app.stage}`}.electric-sql.com`, - dns: sst.cloudflare.dns(), - }, - }) -} - -async function addDatabaseToElectric( - uri: string -): Promise<{ id: string; source_secret: string }> { - const adminApi = process.env.ELECTRIC_ADMIN_API - const teamId = process.env.ELECTRIC_TEAM_ID - - const result = await fetch(`${adminApi}/v1/sources`, { - method: `PUT`, - headers: { - "Content-Type": `application/json`, - "CF-Access-Client-Id": process.env.ELECTRIC_ADMIN_API_TOKEN_ID!, - "CF-Access-Client-Secret": process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET!, - }, - body: JSON.stringify({ - database_url: uri, - region: `us-east-1`, - team_id: teamId, - }), - }) - - if (!result.ok) { - throw new Error( - `Could not add database to Electric (${result.status}): ${await result.text()}` - ) - } - - return await result.json() -} - -function getNeonConnectionString({ - project, - roleName, - databaseName, - pooled, -}: { - project: $util.Output - roleName: $util.Input - databaseName: $util.Input - pooled: boolean -}): $util.Output { - const passwordOutput = neon.getBranchRolePasswordOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - roleName: roleName, - }) - - const endpoint = neon.getBranchEndpointsOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - }) - const databaseHost = pooled - ? endpoint.endpoints?.apply((endpoints) => - endpoints![0].host.replace( - endpoints![0].id, - endpoints![0].id + `-pooler` - ) - ) - : project.databaseHost - return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` -} - -/** - * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with - * a Pulumi Command resource and `curl` to create and delete Neon databases. - */ -function createNeonDb({ - projectId, - branchId, - dbName, -}: { - projectId: $util.Input - branchId: $util.Input - dbName: $util.Input -}): $util.Output<{ - dbName: string - ownerName: string -}> { - if (!process.env.NEON_API_KEY) { - throw new Error(`NEON_API_KEY is not set`) - } - - const ownerName = `neondb_owner` - - const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - -H 'Content-Type: application/json' \ - -d '{ - "database": { - "name": "'$DATABASE_NAME'", - "owner_name": "${ownerName}" - } - }' \ - && echo " SUCCESS" || echo " FAILURE"` - - const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` - - const deleteCommand = `curl -f -s -X 'DELETE' \ - "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - && echo " SUCCESS" || echo " FAILURE"` - - const result = new command.local.Command(`neon-db-command:${dbName}`, { - create: createCommand, - update: updateCommand, - delete: deleteCommand, - environment: { - NEON_API_KEY: process.env.NEON_API_KEY, - PROJECT_ID: projectId, - BRANCH_ID: branchId, - DATABASE_NAME: dbName, - }, - }) - return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { - if (stdout.endsWith(`SUCCESS`)) { - console.log(`Created Neon database ${dbName}`) - return { - dbName, - ownerName, - } - } else { - throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) - } - }) -} diff --git a/examples/proxy-auth/package.json b/examples/proxy-auth/package.json index d8316d9a90..e56577f292 100644 --- a/examples/proxy-auth/package.json +++ b/examples/proxy-auth/package.json @@ -23,7 +23,7 @@ "pg": "^8.12.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "sst": "3.3.64", + "sst": "3.6.35", "uuid": "^10.0.0", "zod": "^3.23.8" }, diff --git a/examples/proxy-auth/sst.config.ts b/examples/proxy-auth/sst.config.ts index 5e8eeb27ab..17ee79cc70 100644 --- a/examples/proxy-auth/sst.config.ts +++ b/examples/proxy-auth/sst.config.ts @@ -1,17 +1,15 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import { execSync } from "child_process" -const isProduction = (stage: string) => stage.toLowerCase() === `production` - -const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID -const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET +import { createDatabaseForCloudElectric } from "../.shared/lib/database" +import { isProduction } from "../.shared/lib/infra" export default $config({ app(input) { return { name: `proxy-auth`, - removal: input?.stage === `production` ? `retain` : `remove`, + removal: + input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`, protect: [`production`].includes(input?.stage), home: `aws`, providers: { @@ -28,198 +26,34 @@ export default $config({ } }, async run() { - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_ID) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - if (!process.env.ELECTRIC_API || !process.env.ELECTRIC_ADMIN_API) + if (!process.env.ELECTRIC_API) throw new Error( `Env variables ELECTRIC_API and ELECTRIC_ADMIN_API must be set` ) - const project = neon.getProjectOutput({ - id: process.env.NEON_PROJECT_ID!, - }) - - const dbName = isProduction($app.stage) + const dbName = isProduction() ? `proxy-auth-production` : `proxy-auth-${$app.stage}` - const { ownerName, dbName: resultingDbName } = createNeonDb({ - projectId: project.id, - branchId: project.defaultBranchId, + const { sourceId, sourceSecret } = createDatabaseForCloudElectric({ dbName, - }) - - const databaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: false, - }) - - const electricInfo = databaseUri.apply((uri) => { - return addDatabaseToElectric(uri) + migrationsDirectory: `./db/migrations`, }) const staticSite = new sst.aws.Nextjs(`proxy-auth`, { environment: { ELECTRIC_URL: process.env.ELECTRIC_API!, - ELECTRIC_SOURCE_SECRET: electricInfo.source_secret, - ELECTRIC_SOURCE_ID: electricInfo.id, + ELECTRIC_SOURCE_SECRET: sourceSecret, + ELECTRIC_SOURCE_ID: sourceId, }, domain: { - name: `proxy-auth${isProduction($app.stage) ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, + name: `proxy-auth${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, dns: sst.cloudflare.dns(), }, }) - databaseUri.apply(applyMigrations) - return { - databaseUri, - // source_id: electricInfo.id, - // source_secret: electricInfo.source_secret, website: 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 -): Promise<{ id: string; source_secret: string }> { - const adminApi = process.env.ELECTRIC_ADMIN_API - const teamId = process.env.ELECTRIC_TEAM_ID - - const result = await fetch(`${adminApi}/v1/sources`, { - method: `PUT`, - headers: { - "Content-Type": `application/json`, - "CF-Access-Client-Id": adminApiTokenId ?? ``, - "CF-Access-Client-Secret": adminApiTokenSecret ?? ``, - }, - body: JSON.stringify({ - database_url: uri, - region: `us-east-1`, - team_id: teamId, - }), - }) - - if (!result.ok) { - throw new Error( - `Could not add database to Electric (${result.status}): ${await result.text()}` - ) - } - - return await result.json() -} - -function getNeonConnectionString({ - project, - roleName, - databaseName, - pooled, -}: { - project: $util.Output - roleName: $util.Input - databaseName: $util.Input - pooled: boolean -}): $util.Output { - const passwordOutput = neon.getBranchRolePasswordOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - roleName: roleName, - }) - - const endpoint = neon.getBranchEndpointsOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - }) - const databaseHost = pooled - ? endpoint.endpoints?.apply((endpoints) => - endpoints![0].host.replace( - endpoints![0].id, - endpoints![0].id + `-pooler` - ) - ) - : project.databaseHost - return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` -} - -/** - * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with - * a Pulumi Command resource and `curl` to create and delete Neon databases. - */ -function createNeonDb({ - projectId, - branchId, - dbName, -}: { - projectId: $util.Input - branchId: $util.Input - dbName: $util.Input -}): $util.Output<{ - dbName: string - ownerName: string -}> { - if (!process.env.NEON_API_KEY) { - throw new Error(`NEON_API_KEY is not set`) - } - - const ownerName = `neondb_owner` - - const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - -H 'Content-Type: application/json' \ - -d '{ - "database": { - "name": "'$DATABASE_NAME'", - "owner_name": "${ownerName}" - } - }' \ - && echo " SUCCESS" || echo " FAILURE"` - - const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` - - const deleteCommand = `curl -f -s -X 'DELETE' \ - "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - && echo " SUCCESS" || echo " FAILURE"` - - const result = new command.local.Command(`neon-db-command:${dbName}`, { - create: createCommand, - update: updateCommand, - delete: deleteCommand, - environment: { - NEON_API_KEY: process.env.NEON_API_KEY, - PROJECT_ID: projectId, - BRANCH_ID: branchId, - DATABASE_NAME: dbName, - }, - }) - return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { - if (stdout.endsWith(`SUCCESS`)) { - console.log(`Created Neon database ${dbName}`) - return { - dbName, - ownerName, - } - } else { - throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) - } - }) -} diff --git a/examples/todo-app/package.json b/examples/todo-app/package.json index 811d2aaaf6..2720445148 100644 --- a/examples/todo-app/package.json +++ b/examples/todo-app/package.json @@ -18,7 +18,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.23.1", - "sst": "3.3.7", + "sst": "3.6.35", "uuid": "^10.0.0", "zod": "^3.23.8" }, diff --git a/examples/todo-app/sst.config.ts b/examples/todo-app/sst.config.ts index 586d4762e5..a14ef7b49e 100644 --- a/examples/todo-app/sst.config.ts +++ b/examples/todo-app/sst.config.ts @@ -1,19 +1,15 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import { execSync } from "child_process" - -const isProduction = (stage: string) => - stage.toLocaleLowerCase() === `production` - -const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID -const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET +import { createDatabaseForCloudElectric } from "../.shared/lib/database" +import { getSharedCluster, isProduction } from "../.shared/lib/infra" export default $config({ app(input) { return { name: `todo-app-example`, - removal: isProduction(input?.stage) ? `retain` : `remove`, + removal: + input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`, home: `aws`, providers: { cloudflare: `5.42.0`, @@ -27,238 +23,64 @@ export default $config({ } }, async run() { - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_ID) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - try { - const project = neon.getProjectOutput({ - id: process.env.NEON_PROJECT_ID!, - }) + const dbName = isProduction() ? `todo-app` : `todo-app-${$app.stage}` - const dbName = isProduction($app.stage) - ? `todo-app` - : `todo-app-${$app.stage}` - - const { ownerName, dbName: resultingDbName } = createNeonDb({ - projectId: project.id, - branchId: project.defaultBranchId, + const { pooledDatabaseUri, sourceId, sourceSecret } = + createDatabaseForCloudElectric({ dbName, + migrationsDirectory: `./db/migrations`, }) - const databaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: false, - }) - - databaseUri.apply(applyMigrations) - - const electricInfo = databaseUri.apply((uri) => - addDatabaseToElectric(uri) - ) - - const vpc = new sst.aws.Vpc(`todo-app-${$app.stage}-vpc`) - const cluster = new sst.aws.Cluster(`todo-app-${$app.stage}-cluster`, { - vpc, - }) - - const service = cluster.addService(`todo-app-service-${$app.stage}`, { - loadBalancer: { - ports: [{ listen: "443/https", forward: "3010/http" }], - domain: { - name: `todo-app-backend${isProduction($app.stage) ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, - dns: sst.cloudflare.dns(), - }, - }, - environment: { - DATABASE_URL: databaseUri, - }, - image: { - context: "../..", - dockerfile: "Dockerfile", - }, - dev: { - command: "node server.js", - }, - }) - - if (!process.env.ELECTRIC_API) { - throw new Error(`ELECTRIC_API environment variable is required`) - } - - const website = new sst.aws.StaticSite("todo-app-website", { - build: { - command: "npm run build", - output: "dist", - }, - environment: { - VITE_SERVER_URL: service.url.apply((url) => - url.slice(0, url.length - 1) - ), - VITE_ELECTRIC_URL: process.env.ELECTRIC_API, - VITE_ELECTRIC_SOURCE_SECRET: electricInfo.source_secret, - VITE_ELECTRIC_SOURCE_ID: electricInfo.id, - }, + const cluster = getSharedCluster(`todo-app-${$app.stage}`) + const service = cluster.addService(`todo-app-${$app.stage}-service`, { + loadBalancer: { + ports: [{ listen: "443/https", forward: "3010/http" }], domain: { - name: `todo-app${isProduction($app.stage) ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, + name: `todo-app-backend${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, dns: sst.cloudflare.dns(), }, - dev: { - command: "npm run vite", - }, - }) + }, + environment: { + DATABASE_URL: pooledDatabaseUri, + }, + image: { + context: "../..", + dockerfile: "Dockerfile", + }, + dev: { + command: "node server.js", + }, + }) - return { - databaseUri, - // source_id: electricInfo.id, - // source_secret: electricInfo.source_secret, - server: service.url, - website: website.url, - } - } catch (e) { - console.error(`Failed to deploy todo app ${$app.stage} stack`, e) + if (!process.env.ELECTRIC_API) { + throw new Error(`ELECTRIC_API environment variable is required`) } - }, -}) - -function applyMigrations(uri: string) { - execSync(`pnpm exec pg-migrations apply --directory ./db/migrations`, { - env: { - ...process.env, - DATABASE_URL: uri, - }, - }) -} - -async function addDatabaseToElectric( - uri: string -): Promise<{ id: string; source_secret: string }> { - const adminApi = process.env.ELECTRIC_ADMIN_API - const teamId = process.env.ELECTRIC_TEAM_ID - - const result = await fetch(`${adminApi}/v1/sources`, { - method: `PUT`, - headers: { - "Content-Type": `application/json`, - "CF-Access-Client-Id": adminApiTokenId ?? ``, - "CF-Access-Client-Secret": adminApiTokenSecret ?? ``, - }, - body: JSON.stringify({ - database_url: uri, - region: `us-east-1`, - team_id: teamId, - }), - }) - - if (!result.ok) { - throw new Error( - `Could not add database to Electric (${result.status}): ${await result.text()}` - ) - } - - return await result.json() -} - -function getNeonConnectionString({ - project, - roleName, - databaseName, - pooled, -}: { - project: $util.Output - roleName: $util.Input - databaseName: $util.Input - pooled: boolean -}): $util.Output { - const passwordOutput = neon.getBranchRolePasswordOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - roleName: roleName, - }) - - const endpoint = neon.getBranchEndpointsOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - }) - const databaseHost = pooled - ? endpoint.endpoints?.apply((endpoints) => - endpoints![0].host.replace( - endpoints![0].id, - endpoints![0].id + `-pooler` - ) - ) - : project.databaseHost - return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` -} -/** - * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with - * a Pulumi Command resource and `curl` to create and delete Neon databases. - */ -function createNeonDb({ - projectId, - branchId, - dbName, -}: { - projectId: $util.Input - branchId: $util.Input - dbName: $util.Input -}): $util.Output<{ - dbName: string - ownerName: string -}> { - if (!process.env.NEON_API_KEY) { - throw new Error(`NEON_API_KEY is not set`) - } - - const ownerName = `neondb_owner` - - const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - -H 'Content-Type: application/json' \ - -d '{ - "database": { - "name": "'$DATABASE_NAME'", - "owner_name": "${ownerName}" - } - }' \ - && echo " SUCCESS" || echo " FAILURE"` - - const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` - - const deleteCommand = `curl -f -s -X 'DELETE' \ - "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - && echo " SUCCESS" || echo " FAILURE"` + const website = new sst.aws.StaticSite("todo-app-website", { + build: { + command: "npm run build", + output: "dist", + }, + environment: { + VITE_SERVER_URL: service.url.apply((url) => + url.slice(0, url.length - 1) + ), + VITE_ELECTRIC_URL: process.env.ELECTRIC_API, + VITE_ELECTRIC_SOURCE_SECRET: sourceSecret, + VITE_ELECTRIC_SOURCE_ID: sourceId, + }, + domain: { + name: `todo-app${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`, + dns: sst.cloudflare.dns(), + }, + dev: { + command: "npm run vite", + }, + }) - const result = new command.local.Command(`neon-db-command:${dbName}`, { - create: createCommand, - update: updateCommand, - delete: deleteCommand, - environment: { - NEON_API_KEY: process.env.NEON_API_KEY, - PROJECT_ID: projectId, - BRANCH_ID: branchId, - DATABASE_NAME: dbName, - }, - }) - return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { - if (stdout.endsWith(`SUCCESS`)) { - console.log(`Created Neon database ${dbName}`) - return { - dbName, - ownerName, - } - } else { - throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) + return { + server: service.url, + website: website.url, } - }) -} + }, +}) diff --git a/examples/write-patterns/package.json b/examples/write-patterns/package.json index 11fae239c0..73e2c67287 100644 --- a/examples/write-patterns/package.json +++ b/examples/write-patterns/package.json @@ -18,7 +18,7 @@ "pg": "^8.12.0", "react": "19.0.0-rc.1", "react-dom": "19.0.0-rc.1", - "sst": "3.3.64", + "sst": "3.6.35", "uuid": "^10.0.0", "valtio": "^2.1.2", "zod": "^3.23.8" diff --git a/examples/write-patterns/sst.config.ts b/examples/write-patterns/sst.config.ts index 1ddbbb7642..72c9138b9e 100644 --- a/examples/write-patterns/sst.config.ts +++ b/examples/write-patterns/sst.config.ts @@ -2,21 +2,18 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import { execSync } from 'child_process' - -const isProduction = (stage) => stage.toLocaleLowerCase() === 'production' - -const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID -const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET +import { createDatabaseForCloudElectric } from '../.shared/lib/database' +import { getSharedCluster, isProduction } from '../.shared/lib/infra' export default $config({ app(input) { return { name: 'write-patterns', - removal: input?.stage === 'production' ? 'retain' : 'remove', + removal: + input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`, home: 'aws', providers: { - cloudflare: '5.42.0', + cloudflare: `5.42.0`, aws: { version: `6.66.2`, profile: process.env.CI ? undefined : `marketing`, @@ -27,246 +24,71 @@ export default $config({ } }, async run() { - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_ID) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - const project = neon.getProjectOutput({ id: process.env.NEON_PROJECT_ID! }) - - const dbName = isProduction($app.stage) + const dbName = isProduction() ? 'write-patterns-production' : `write-patterns-${$app.stage}` - const { ownerName, dbName: resultingDbName } = createNeonDb({ - projectId: project.id, - branchId: project.defaultBranchId, - dbName, - }) - - const databaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: false, - }) - - try { - databaseUri.apply(applyMigrations) - - const electricInfo = databaseUri.apply((uri) => - addDatabaseToElectric(uri) - ) - - const vpc = new sst.aws.Vpc(`write-patterns-${$app.stage}-vpc`) - const cluster = new sst.aws.Cluster( - `write-patterns-${$app.stage}-cluster`, - { - vpc, - } - ) - - const service = cluster.addService( - `write-patterns-service-${$app.stage}`, - { - loadBalancer: { - ports: [{ listen: '443/https', forward: '3001/http' }], - domain: { - name: `write-patterns-backend${ - $app.stage === 'production' ? '' : `-stage-${$app.stage}` - }.examples.electric-sql.com`, - dns: sst.cloudflare.dns(), - }, - }, - environment: { - DATABASE_URL: databaseUri, - }, - image: { - context: '../..', - dockerfile: 'Dockerfile', - }, - dev: { - command: 'node server.js', - }, - } - ) + const { pooledDatabaseUri, sourceId, sourceSecret } = + createDatabaseForCloudElectric({ + dbName, + migrationsDirectory: `./shared/migrations`, + }) - if (!process.env.ELECTRIC_API) { - throw new Error('ELECTRIC_API environment variable is required') - } + const cluster = getSharedCluster(`write-patterns-${$app.stage}`) - const website = new sst.aws.StaticSite('write-patterns-website', { - build: { - command: 'npm run build', - output: 'dist', - }, - environment: { - VITE_SERVER_URL: service.url.apply((url) => - url.slice(0, url.length - 1) - ), - VITE_ELECTRIC_URL: process.env.ELECTRIC_API, - VITE_ELECTRIC_SOURCE_ID: electricInfo.id, - VITE_ELECTRIC_SOURCE_SECRET: electricInfo.source_secret, - }, + const service = cluster.addService(`write-patterns-${$app.stage}-service`, { + loadBalancer: { + ports: [{ listen: '443/https', forward: '3001/http' }], domain: { - name: `write-patterns${ - isProduction($app.stage) ? '' : `-stage-${$app.stage}` + name: `write-patterns-backend${ + isProduction() ? '' : `-stage-${$app.stage}` }.examples.electric-sql.com`, dns: sst.cloudflare.dns(), }, - dev: { - command: 'npm run vite', - }, - }) + }, + environment: { + DATABASE_URL: pooledDatabaseUri, + }, + image: { + context: '../..', + dockerfile: 'Dockerfile', + }, + dev: { + command: 'node server.js', + }, + }) - return { - databaseUri, - // source_id: electricInfo.id, - // source_secret: electricInfo.source_secret, - server: service.url, - website: website.url, - } - } catch (e) { - console.error('Failed to deploy todo app example stack', e) + if (!process.env.ELECTRIC_API) { + throw new Error('ELECTRIC_API environment variable is required') } - }, -}) - -function applyMigrations(uri: string) { - execSync('pnpm exec pg-migrations apply --directory ./shared/migrations', { - env: { - ...process.env, - DATABASE_URL: uri, - }, - }) -} - -async function addDatabaseToElectric( - uri: string -): Promise<{ id: string; source_secret: string }> { - const adminApi = process.env.ELECTRIC_ADMIN_API - const teamId = process.env.ELECTRIC_TEAM_ID - - const result = await fetch(`${adminApi}/v1/sources`, { - method: `PUT`, - headers: { - 'Content-Type': `application/json`, - 'CF-Access-Client-Id': adminApiTokenId ?? ``, - 'CF-Access-Client-Secret': adminApiTokenSecret ?? ``, - }, - body: JSON.stringify({ - database_url: uri, - region: `us-east-1`, - team_id: teamId, - }), - }) - - if (!result.ok) { - throw new Error( - `Could not add database to Electric (${result.status}): ${await result.text()}` - ) - } - - return await result.json() -} - -function getNeonConnectionString({ - project, - roleName, - databaseName, - pooled, -}: { - project: $util.Output - roleName: $util.Input - databaseName: $util.Input - pooled: boolean -}): $util.Output { - const passwordOutput = neon.getBranchRolePasswordOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - roleName: roleName, - }) - - const endpoint = neon.getBranchEndpointsOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - }) - const databaseHost = pooled - ? endpoint.endpoints?.apply((endpoints) => - endpoints![0].host.replace( - endpoints![0].id, - endpoints![0].id + `-pooler` - ) - ) - : project.databaseHost - return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` -} - -/** - * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with - * a Pulumi Command resource and `curl` to create and delete Neon databases. - */ -function createNeonDb({ - projectId, - branchId, - dbName, -}: { - projectId: $util.Input - branchId: $util.Input - dbName: $util.Input -}): $util.Output<{ - dbName: string - ownerName: string -}> { - if (!process.env.NEON_API_KEY) { - throw new Error(`NEON_API_KEY is not set`) - } - - const ownerName = `neondb_owner` - const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - -H 'Content-Type: application/json' \ - -d '{ - "database": { - "name": "'$DATABASE_NAME'", - "owner_name": "${ownerName}" - } - }' \ - && echo " SUCCESS" || echo " FAILURE"` - - const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` - - const deleteCommand = `curl -f -s -X 'DELETE' \ - "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - && echo " SUCCESS" || echo " FAILURE"` + const website = new sst.aws.StaticSite('write-patterns-website', { + build: { + command: 'npm run build', + output: 'dist', + }, + environment: { + VITE_SERVER_URL: service.url.apply((url) => + url.slice(0, url.length - 1) + ), + VITE_ELECTRIC_URL: process.env.ELECTRIC_API, + VITE_ELECTRIC_SOURCE_ID: sourceId, + VITE_ELECTRIC_SOURCE_SECRET: sourceSecret, + }, + domain: { + name: `write-patterns${ + isProduction() ? '' : `-stage-${$app.stage}` + }.examples.electric-sql.com`, + dns: sst.cloudflare.dns(), + }, + dev: { + command: 'npm run vite', + }, + }) - const result = new command.local.Command(`neon-db-command:${dbName}`, { - create: createCommand, - update: updateCommand, - delete: deleteCommand, - environment: { - NEON_API_KEY: process.env.NEON_API_KEY, - PROJECT_ID: projectId, - BRANCH_ID: branchId, - DATABASE_NAME: dbName, - }, - }) - return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { - if (stdout.endsWith(`SUCCESS`)) { - console.log(`Created Neon database ${dbName}`) - return { - dbName, - ownerName, - } - } else { - throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) + return { + server: service.url, + website: website.url, } - }) -} + }, +}) diff --git a/examples/yjs/package.json b/examples/yjs/package.json index e16967c1da..9d630ed1b9 100644 --- a/examples/yjs/package.json +++ b/examples/yjs/package.json @@ -29,7 +29,7 @@ "pg": "^8.13.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "sst": "3.6.25", + "sst": "3.6.35", "y-codemirror.next": "0.3.5", "y-indexeddb": "^9.0.12", "y-protocols": "1.0.6", diff --git a/examples/yjs/sst-env.d.ts b/examples/yjs/sst-env.d.ts index 95ab3fbb9b..c1b62bdd43 100644 --- a/examples/yjs/sst-env.d.ts +++ b/examples/yjs/sst-env.d.ts @@ -2,18 +2,12 @@ /* tslint:disable */ /* eslint-disable */ /* deno-fmt-ignore-file */ -import "sst" -export {} + declare module "sst" { export interface Resource { - "yjs-service-production": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "yjs-vpc-production": { - "bastion": string - "type": "sst.aws.Vpc" - } } } +/// + +import "sst" +export {} \ No newline at end of file diff --git a/examples/yjs/sst.config.ts b/examples/yjs/sst.config.ts index a85cdf87ef..d20c039ebf 100644 --- a/examples/yjs/sst.config.ts +++ b/examples/yjs/sst.config.ts @@ -1,12 +1,8 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -import { execSync } from "child_process" - -const isProduction = () => $app.stage.toLocaleLowerCase() === `production` - -const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID -const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET +import { getSharedCluster, isProduction } from "../.shared/lib/infra" +import { createDatabaseForCloudElectric } from "../.shared/lib/database" export default $config({ app(input) { @@ -27,225 +23,41 @@ export default $config({ } }, async run() { - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_ID) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } - - if (!$dev && !process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET) { - throw new Error(`ELECTRIC_ADMIN_API_TOKEN_ID is not set`) - } + const dbName = isProduction() ? `yjs` : `yjs-db-${$app.stage}` - try { - const project = neon.getProjectOutput({ - id: process.env.NEON_PROJECT_ID!, - }) - - const dbName = isProduction() ? `yjs` : `yjs-db-${$app.stage}` - - const { ownerName, dbName: resultingDbName } = createNeonDb({ - projectId: project.id, - branchId: project.defaultBranchId, + const { pooledDatabaseUri, sourceId, sourceSecret } = + createDatabaseForCloudElectric({ dbName, + migrationsDirectory: `./db/migrations`, }) - const pooledUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: true, - }) - const databaseUri = getNeonConnectionString({ - project, - roleName: ownerName, - databaseName: resultingDbName, - pooled: false, - }) - - databaseUri.apply(applyMigrations) - - const electricInfo = databaseUri.apply((uri) => - addDatabaseToElectric(uri) - ) + const cluster = getSharedCluster(`yjs-${$app.stage}`) - // const serverless = deployServerlessApp(electricInfo, pooledUri) - const website = deployAppServer(electricInfo, databaseUri) + const service = cluster.addService(`yjs-${$app.stage}-service`, { + loadBalancer: { + ports: [{ listen: `443/https`, forward: `3000/http` }], + domain: { + name: `yjs${isProduction() ? `` : `-${$app.stage}`}.examples.electric-sql.com`, + dns: sst.cloudflare.dns(), + }, + }, + environment: { + ELECTRIC_URL: process.env.ELECTRIC_API!, + DATABASE_URL: pooledDatabaseUri, + ELECTRIC_SOURCE_ID: sourceId, + ELECTRIC_SOURCE_SECRET: sourceSecret, + }, + image: { + context: `../..`, + dockerfile: `Dockerfile`, + }, + dev: { + command: `npm run dev`, + }, + }) - return { - // serverless_url: serverless.url, - website: website.url, - databaseUri, - databasePooledUri: pooledUri, - } - } catch (e) { - console.error(e) + return { + website: service.url, } }, }) - -function applyMigrations(uri: string) { - execSync(`pnpm exec pg-migrations apply --directory ./db/migrations`, { - env: { - ...process.env, - DATABASE_URL: uri, - }, - }) -} - -function deployAppServer( - { id, source_secret }: $util.Output<{ id: string; source_secret: string }>, - uri: $util.Output -) { - const vpc = new sst.aws.Vpc(`yjs-vpc-${$app.stage}`, { bastion: true }) - const cluster = new sst.aws.Cluster(`yjs-cluster-${$app.stage}`, { vpc }) - const service = cluster.addService(`yjs-service-${$app.stage}`, { - loadBalancer: { - ports: [{ listen: `443/https`, forward: `3000/http` }], - domain: { - name: `yjs${isProduction() ? `` : `-${$app.stage}`}.examples.electric-sql.com`, - dns: sst.cloudflare.dns(), - }, - }, - environment: { - ELECTRIC_URL: process.env.ELECTRIC_API!, - DATABASE_URL: uri, - ELECTRIC_SOURCE_ID: id, - ELECTRIC_SOURCE_SECRET: source_secret, - }, - image: { - context: `../..`, - dockerfile: `Dockerfile`, - }, - dev: { - command: `npm run dev`, - }, - }) - - return service -} - -async function addDatabaseToElectric( - uri: string -): Promise<{ id: string; source_secret: string }> { - const adminApi = process.env.ELECTRIC_ADMIN_API - const teamId = process.env.ELECTRIC_TEAM_ID - - const result = await fetch(`${adminApi}/v1/sources`, { - method: `PUT`, - headers: { - "Content-Type": `application/json`, - "CF-Access-Client-Id": adminApiTokenId ?? ``, - "CF-Access-Client-Secret": adminApiTokenSecret ?? ``, - }, - body: JSON.stringify({ - database_url: uri, - region: `us-east-1`, - team_id: teamId, - }), - }) - - if (!result.ok) { - throw new Error( - `Could not add database to Electric (${result.status}): ${await result.text()}` - ) - } - - return await result.json() -} - -function getNeonConnectionString({ - project, - roleName, - databaseName, - pooled, -}: { - project: $util.Output - roleName: $util.Input - databaseName: $util.Input - pooled: boolean -}): $util.Output { - const passwordOutput = neon.getBranchRolePasswordOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - roleName: roleName, - }) - - const endpoint = neon.getBranchEndpointsOutput({ - projectId: project.id, - branchId: project.defaultBranchId, - }) - const databaseHost = pooled - ? endpoint.endpoints?.apply((endpoints) => - endpoints![0].host.replace( - endpoints![0].id, - endpoints![0].id + `-pooler` - ) - ) - : project.databaseHost - return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require` -} - -/** - * Uses the [Neon API](https://neon.tech/docs/manage/databases) along with - * a Pulumi Command resource and `curl` to create and delete Neon databases. - */ -function createNeonDb({ - projectId, - branchId, - dbName, -}: { - projectId: $util.Input - branchId: $util.Input - dbName: $util.Input -}): $util.Output<{ - dbName: string - ownerName: string -}> { - if (!process.env.NEON_API_KEY) { - throw new Error(`NEON_API_KEY is not set`) - } - - const ownerName = `neondb_owner` - - const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - -H 'Content-Type: application/json' \ - -d '{ - "database": { - "name": "'$DATABASE_NAME'", - "owner_name": "${ownerName}" - } - }' \ - && echo " SUCCESS" || echo " FAILURE"` - - const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"` - - const deleteCommand = `curl -f -s -X 'DELETE' \ - "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $NEON_API_KEY" \ - && echo " SUCCESS" || echo " FAILURE"` - - const result = new command.local.Command(`neon-db-command:${dbName}`, { - create: createCommand, - update: updateCommand, - delete: deleteCommand, - environment: { - NEON_API_KEY: process.env.NEON_API_KEY, - PROJECT_ID: projectId, - BRANCH_ID: branchId, - DATABASE_NAME: dbName, - }, - }) - return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => { - if (stdout.endsWith(`SUCCESS`)) { - console.log(`Created Neon database ${dbName}`) - return { - dbName, - ownerName, - } - } else { - throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`) - } - }) -} diff --git a/package.json b/package.json index f3d90741f8..aff3bfbc53 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,6 @@ "@changesets/cli": "^2.27.10", "dotenv-cli": "^7.4.2" }, - "engines": { - "node": "^20.18.1" - }, "packageManager": "pnpm@9.15.0", "private": true, "scripts": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e06da36816..2731324daf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,36 @@ importers: specifier: ^10.3.10 version: 10.4.5 + examples/.shared: + devDependencies: + '@databases/pg-migrations': + specifier: ^5.0.3 + version: 5.0.3(typescript@5.6.3) + '@types/node': + specifier: ^20.14.10 + version: 20.17.6 + '@types/pg': + specifier: ^8.11.6 + version: 8.11.10 + camelcase: + specifier: ^8.0.0 + version: 8.0.0 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + eslint: + specifier: ^8.57.0 + version: 8.57.1 + pg: + specifier: ^8.13.1 + version: 8.13.1 + sst: + specifier: 3.6.25 + version: 3.6.25 + typescript: + specifier: ^5.5.3 + version: 5.6.3 + examples/bash: {} examples/encryption: @@ -318,8 +348,8 @@ importers: specifier: ^8.4.39 version: 8.4.47 sst: - specifier: 3.3.7 - version: 3.3.7(hono@4.6.13) + specifier: 3.6.35 + version: 3.6.35 supabase: specifier: ^1.226.3 version: 1.226.4 @@ -520,8 +550,8 @@ importers: specifier: ^8.4.39 version: 8.4.47 sst: - specifier: 3.3.7 - version: 3.3.7(hono@4.6.13) + specifier: 3.6.35 + version: 3.6.35 tailwindcss: specifier: ^3.4.4 version: 3.4.14 @@ -542,7 +572,7 @@ importers: version: link:../../packages/react-hooks next: specifier: ^14.2.5 - version: 14.2.17(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pg: specifier: ^8.12.0 version: 8.13.1 @@ -584,8 +614,8 @@ importers: specifier: ^8.57.0 version: 8.57.1 sst: - specifier: 3.3.7 - version: 3.3.7(hono@4.6.13) + specifier: 3.6.35 + version: 3.6.35 typescript: specifier: ^5.5.3 version: 5.6.3 @@ -603,7 +633,7 @@ importers: version: link:../../packages/react-hooks next: specifier: ^14.2.5 - version: 14.2.17(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pg: specifier: ^8.12.0 version: 8.13.1 @@ -614,8 +644,8 @@ importers: 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) + specifier: 3.6.35 + version: 3.6.35 uuid: specifier: ^10.0.0 version: 10.0.0 @@ -911,8 +941,8 @@ importers: specifier: ^6.23.1 version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) sst: - specifier: 3.3.7 - version: 3.3.7(hono@4.6.13) + specifier: 3.6.35 + version: 3.6.35 uuid: specifier: ^10.0.0 version: 10.0.0 @@ -1014,8 +1044,8 @@ importers: specifier: 19.0.0-rc.1 version: 19.0.0-rc.1(react@19.0.0-rc.1) sst: - specifier: 3.3.64 - version: 3.3.64(hono@4.6.13) + specifier: 3.6.35 + version: 3.6.35 uuid: specifier: ^10.0.0 version: 10.0.0 @@ -1091,7 +1121,7 @@ importers: version: 0.2.99 next: specifier: ^14.2.9 - version: 14.2.17(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pg: specifier: ^8.13.1 version: 8.13.1 @@ -1102,8 +1132,8 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) sst: - specifier: 3.6.25 - version: 3.6.25 + specifier: 3.6.35 + version: 3.6.35 y-codemirror.next: specifier: 0.3.5 version: 0.3.5(@codemirror/state@6.5.0)(@codemirror/view@6.35.2)(yjs@13.6.20) @@ -5267,6 +5297,10 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} @@ -8836,29 +8870,14 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - sst-darwin-arm64@3.3.64: - resolution: {integrity: sha512-sGlGX66vjWuN/501zjjEdqIalteyuawuoUvbIQsYFZ3yYg+THSPsXR5e3ADo2LgrOelaMhOOAZ8jabGGEsptnw==} - cpu: [arm64] - os: [darwin] - - sst-darwin-arm64@3.3.7: - resolution: {integrity: sha512-2CQh78YIdvrRpO8enZ/Jx51JsUSFtk564u9w4ldcu5SsMMDY1ocdw5p/XIwBy1eKeRtrXLizd35sYbtSfSy6sw==} - cpu: [arm64] - os: [darwin] - sst-darwin-arm64@3.6.25: resolution: {integrity: sha512-vUf0OXeSIrPN7QPHSxYJcXqycBvcoZJ9scsV1VrTRQbYpySfWCJMD8LSAuRB+//QjcDYndSx/r/ad0Ofx+a2vw==} cpu: [arm64] os: [darwin] - sst-darwin-x64@3.3.64: - resolution: {integrity: sha512-qH8vDyPMbRASBt5T5+dIyheJ7DoGjHaeGBWzloDVN9Ukz+H3W37rSR4cWEkZlYAwxx32DoiFl6hNIhH39TFpAw==} - cpu: [x64] - os: [darwin] - - sst-darwin-x64@3.3.7: - resolution: {integrity: sha512-+hiDT3+am+CBO3xBy8yl3bmFeTjGXUT/+7V6NFOV2yxlRP3A8J65nEjWdzPTU/u7hRl+leE8EBu14j0grt/7/A==} - cpu: [x64] + sst-darwin-arm64@3.6.35: + resolution: {integrity: sha512-1NSas4wm2hSBZbLOLklvRiRw+vC1IttvxrsDgj2XqFesZ/J4NknVacmym2V82JPkAI/TUcKpgf27i4JTEj7NlQ==} + cpu: [arm64] os: [darwin] sst-darwin-x64@3.6.25: @@ -8866,29 +8885,19 @@ packages: cpu: [x64] os: [darwin] - sst-linux-arm64@3.3.64: - resolution: {integrity: sha512-MUd0hHMap0SXAdU+UniAAlAzEzuJNIPbnQ80SHFGKatJNgFlQmmp7jq4qR1GXjaB3/NttLKIroQ8RBXuydtUzA==} - cpu: [arm64] - os: [linux] - - sst-linux-arm64@3.3.7: - resolution: {integrity: sha512-dYolpXAjq0S8QjL8sTKzcRpPNgZDeMcJ9PHnt/8GpdqxNxEpGlNF9gMl2cB7mleJyJYBNMPvi4YEeCGtcazmeQ==} - cpu: [arm64] - os: [linux] + sst-darwin-x64@3.6.35: + resolution: {integrity: sha512-jpFpUqrJGM8BMVj5V6A5x+VWgW7l/n1u7DUoV4HP/GcLW2WbtdN0ug1BjLdfpEGQ50sRzO6XJSZx2t3JFKDtDQ==} + cpu: [x64] + os: [darwin] sst-linux-arm64@3.6.25: resolution: {integrity: sha512-5Xjj08Uo5J1abiCHv09lxA0SqswbP2Vz5y6DAgocTPSLMuUlgCv9m0r+w+ng3OLDoCDVb56EUY0G8zwWX1Q3gA==} cpu: [arm64] os: [linux] - sst-linux-x64@3.3.64: - resolution: {integrity: sha512-8V42iyy9hc2bcW570DCzFIZrL1wOlSx0PF2M+Zlq7jhVGbhBTTZ13HHIo//W7ETtsAhItBeknZil6LidLz4ZDg==} - cpu: [x64] - os: [linux] - - sst-linux-x64@3.3.7: - resolution: {integrity: sha512-K2vPOZ5DS8mJmE4QtffgZN5Nem1MIBhoVozNtZ0NoufeKHbFz0Hyw9wbqxYSbs2MOoVNKvG8qwcX99ojVXTFKw==} - cpu: [x64] + sst-linux-arm64@3.6.35: + resolution: {integrity: sha512-VFFVk1b6wPg/nBgHPUARJZmJQnQKebJr9m+Cu6jDfVyNsIpCo+q1W/uKbia7Y2fSpMMS6Pdw5480CJvmLtz/7g==} + cpu: [arm64] os: [linux] sst-linux-x64@3.6.25: @@ -8896,14 +8905,9 @@ packages: cpu: [x64] os: [linux] - sst-linux-x86@3.3.64: - resolution: {integrity: sha512-TuJ5Zf9Lt5nTUOFKR+/EqPbpmzvtngHzQ5qvVq0k0wgPFC/ZgZItJtgZlspz8LL7wFl5dt2Y7VTVUHpov4PKNQ==} - cpu: [x86] - os: [linux] - - sst-linux-x86@3.3.7: - resolution: {integrity: sha512-4rXj54+UJd+HLmrhCHQ0k9AOkugHZhhh6sCUnkUNChJr5ei62pRscUQ7ge8/jywvfzHZGZw3eXXJWCCsjilXFA==} - cpu: [x86] + sst-linux-x64@3.6.35: + resolution: {integrity: sha512-Iw48glLvm6W6xV+lUus37xdOuzgFnHJ/wFF2fhzsw/NrWo2nmft5YPj7Cg3XQqBGtPR82v0xU/MaPX0PPbu0Zw==} + cpu: [x64] os: [linux] sst-linux-x86@3.6.25: @@ -8911,34 +8915,19 @@ packages: cpu: [x86] os: [linux] - sst@3.3.64: - resolution: {integrity: sha512-Mvgbz/ylG2UOzDir9t0Qk8M0wbzQMzwm0/lk5+lywqpK8emAea43+yOIgPvaJEeGiv9i9T97XRnYLbddUGZAFA==} - hasBin: true - peerDependencies: - hono: 4.x - valibot: 0.30.x - peerDependenciesMeta: - hono: - optional: true - valibot: - optional: true - - sst@3.3.7: - resolution: {integrity: sha512-qIJPQnGeIHarWZoUvphwi6R1nu6Pccd3Q2Qy9ltBLs4Z47TkSdwBNeqCBhgAzWA0eLDwStTXliexyQCcNM6gDQ==} - hasBin: true - peerDependencies: - hono: 4.x - valibot: 0.30.x - peerDependenciesMeta: - hono: - optional: true - valibot: - optional: true + sst-linux-x86@3.6.35: + resolution: {integrity: sha512-I1C+QuL4j0t5whv6GzRSWFcyRGZKIxTNNVS82KYFiJdA6YPMYvTE2Tp5eYixBA1WutQAFBwkC4brhhovJHz1uQ==} + cpu: [x86] + os: [linux] sst@3.6.25: resolution: {integrity: sha512-lEvXCunAEhBQ7jUTj/tF8/5vfbXS1T7nNvfzAo0ZpHE1AH/Pj3vGqI1y4Zxs+SQBfEhUbPKjOH3aDWsOWHCmfA==} hasBin: true + sst@3.6.35: + resolution: {integrity: sha512-dEadN3Z+FlXja0gVF0f8sz0BXrxZhzWz9ODLcSScOonApA2WOBBl36HiClh7+oppFNl+IwKeh3AvXtvjVQt8tg==} + hasBin: true + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -14463,6 +14452,8 @@ snapshots: camelcase@6.3.0: {} + camelcase@8.0.0: {} + camelize@1.0.1: {} caniuse-lite@1.0.30001677: {} @@ -15504,7 +15495,7 @@ snapshots: '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 debug: 4.3.7(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 @@ -17164,7 +17155,7 @@ snapshots: neo-async@2.6.2: {} - next@14.2.17(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.17 '@swc/helpers': 0.5.5 @@ -17174,7 +17165,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(@babel/core@7.26.0)(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.17 '@next/swc-darwin-x64': 14.2.17 @@ -18600,76 +18591,35 @@ snapshots: dependencies: minipass: 7.1.2 - sst-darwin-arm64@3.3.64: - optional: true - - sst-darwin-arm64@3.3.7: - optional: true - sst-darwin-arm64@3.6.25: optional: true - sst-darwin-x64@3.3.64: - optional: true - - sst-darwin-x64@3.3.7: + sst-darwin-arm64@3.6.35: optional: true sst-darwin-x64@3.6.25: optional: true - sst-linux-arm64@3.3.64: - optional: true - - sst-linux-arm64@3.3.7: + sst-darwin-x64@3.6.35: optional: true sst-linux-arm64@3.6.25: optional: true - sst-linux-x64@3.3.64: - optional: true - - sst-linux-x64@3.3.7: + sst-linux-arm64@3.6.35: optional: true sst-linux-x64@3.6.25: optional: true - sst-linux-x86@3.3.64: - optional: true - - sst-linux-x86@3.3.7: + sst-linux-x64@3.6.35: optional: true sst-linux-x86@3.6.25: optional: true - sst@3.3.64(hono@4.6.13): - dependencies: - aws4fetch: 1.0.20 - jose: 5.2.3 - openid-client: 5.6.4 - optionalDependencies: - hono: 4.6.13 - sst-darwin-arm64: 3.3.64 - sst-darwin-x64: 3.3.64 - sst-linux-arm64: 3.3.64 - sst-linux-x64: 3.3.64 - sst-linux-x86: 3.3.64 - - sst@3.3.7(hono@4.6.13): - dependencies: - aws4fetch: 1.0.20 - jose: 5.2.3 - openid-client: 5.6.4 - optionalDependencies: - hono: 4.6.13 - sst-darwin-arm64: 3.3.7 - sst-darwin-x64: 3.3.7 - sst-linux-arm64: 3.3.7 - sst-linux-x64: 3.3.7 - sst-linux-x86: 3.3.7 + sst-linux-x86@3.6.35: + optional: true sst@3.6.25: dependencies: @@ -18683,6 +18633,18 @@ snapshots: sst-linux-x64: 3.6.25 sst-linux-x86: 3.6.25 + sst@3.6.35: + dependencies: + aws4fetch: 1.0.20 + jose: 5.2.3 + openid-client: 5.6.4 + optionalDependencies: + sst-darwin-arm64: 3.6.35 + sst-darwin-x64: 3.6.35 + sst-linux-arm64: 3.6.35 + sst-linux-x64: 3.6.35 + sst-linux-x86: 3.6.35 + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -18810,12 +18772,10 @@ snapshots: stylis: 4.3.2 tslib: 2.6.2 - styled-jsx@5.1.1(@babel/core@7.26.0)(react@18.3.1): + styled-jsx@5.1.1(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 - optionalDependencies: - '@babel/core': 7.26.0 stylis@4.3.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ed625600d..1dd67117df 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - 'packages/*' - 'examples/*' - 'website' + - 'examples/.shared'