diff --git a/.github/workflows/deploy_examples.yml b/.github/workflows/deploy_examples.yml index 7716031984..f7562eb714 100644 --- a/.github/workflows/deploy_examples.yml +++ b/.github/workflows/deploy_examples.yml @@ -1,10 +1,10 @@ name: Deploy Examples on: - push: - branches: ['main'] - pull_request: - paths: ['examples/*/**'] + push: + branches: ["main"] + pull_request: + paths: ["examples/*/**"] concurrency: group: ${{ github.event_name == 'push' && 'prod-deploy-group' || format('examples-pr-{0}', github.event.number) }} @@ -25,6 +25,7 @@ jobs: NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }} ELECTRIC_API: ${{ secrets.ELECTRIC_API }} ELECTRIC_ADMIN_API: ${{ secrets.ELECTRIC_ADMIN_API }} + ELECTRIC_TEAM_ID: ${{ secrets.ELECTRIC_TEAM_ID }} # HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} TODO steps: @@ -33,7 +34,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: ".tool-versions" - cache: 'pnpm' + cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -47,7 +48,7 @@ jobs: sst-cache-main-${{ runner.os }} - name: Deploy Linearlite Read Only - working-directory: examples/linearlite-read-only + working-directory: ./examples/linearlite-read-only run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} if [ -f ".sst/outputs.json" ]; then @@ -58,9 +59,8 @@ jobs: exit 1 fi - - name: Deploy NextJs example - working-directory: examples/nextjs-example + working-directory: ./examples/nextjs run: | pnpm sst deploy --stage ${{ env.DEPLOY_ENV }} if [ -f ".sst/outputs.json" ]; then diff --git a/examples/linearlite-read-only/src/pages/Issue/Comments.tsx b/examples/linearlite-read-only/src/pages/Issue/Comments.tsx index 6d07cfbe3d..36b6b9dfd5 100644 --- a/examples/linearlite-read-only/src/pages/Issue/Comments.tsx +++ b/examples/linearlite-read-only/src/pages/Issue/Comments.tsx @@ -20,7 +20,7 @@ function Comments(commentProps: CommentsProps) { url: `${baseUrl}/v1/shape`, params: { token, - database_id: databaseId, + source_id: databaseId, table: `comment`, }, })! as Comment[] diff --git a/examples/linearlite-read-only/src/shapes.ts b/examples/linearlite-read-only/src/shapes.ts index 6f044f7675..e99cd105a8 100644 --- a/examples/linearlite-read-only/src/shapes.ts +++ b/examples/linearlite-read-only/src/shapes.ts @@ -6,6 +6,6 @@ export const issueShape: ShapeStreamOptions = { params: { table: `issue`, token, - database_id: databaseId, + source_id: databaseId, }, } diff --git a/examples/linearlite-read-only/sst.config.ts b/examples/linearlite-read-only/sst.config.ts index 58a4c1052b..0d58859381 100644 --- a/examples/linearlite-read-only/sst.config.ts +++ b/examples/linearlite-read-only/sst.config.ts @@ -34,7 +34,7 @@ export default $config({ ownerName: `neondb_owner`, }) - const databaseUri = getNeonDbUri(project, db) + const databaseUri = getNeonDbUri(project, db, false) try { databaseUri.apply(applyMigrations) databaseUri.apply(loadData) @@ -96,28 +96,44 @@ function deployLinearLite( function getNeonDbUri( project: $util.Output, - db: neon.Database + db: neon.Database, + pooled: boolean ) { const passwordOutput = neon.getBranchRolePasswordOutput({ projectId: project.id, branchId: project.defaultBranchId, roleName: db.ownerName, }) + 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}@${project.databaseHost}/${db.name}?sslmode=require` + return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${db.name}?sslmode=require` } async function addDatabaseToElectric( uri: string ): Promise<{ id: string; token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID - const result = await fetch(`${adminApi}/v1/databases`, { + const result = await fetch(`${adminApi}/v1/sources`, { method: `PUT`, headers: { 'Content-Type': `application/json` }, body: JSON.stringify({ database_url: uri, region: `us-east-1`, + team_id: teamId, }), }) diff --git a/examples/linearlite/src/sync.ts b/examples/linearlite/src/sync.ts index efdeb662e1..a51e5eb93e 100644 --- a/examples/linearlite/src/sync.ts +++ b/examples/linearlite/src/sync.ts @@ -67,7 +67,7 @@ async function startSyncToDatabase(pg: PGliteWithExtensions) { url: issueUrl.toString(), params: { table: 'issue', - database_id: ELECTRIC_DATABASE_ID, + source_id: ELECTRIC_DATABASE_ID, }, }, table: 'issue', @@ -103,7 +103,7 @@ async function startSyncToDatabase(pg: PGliteWithExtensions) { url: commentUrl.toString(), params: { table: 'comment', - database_id: ELECTRIC_DATABASE_ID, + source_id: ELECTRIC_DATABASE_ID, }, }, table: 'comment', diff --git a/examples/linearlite/sst.config.ts b/examples/linearlite/sst.config.ts index 1de835be3b..3af052f1c0 100644 --- a/examples/linearlite/sst.config.ts +++ b/examples/linearlite/sst.config.ts @@ -78,13 +78,15 @@ async function addDatabaseToElectric( uri: string ): Promise<{ id: string; token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID - const result = await fetch(`${adminApi}/v1/databases`, { + const result = await fetch(`${adminApi}/v1/sources`, { method: `PUT`, headers: { 'Content-Type': `application/json` }, body: JSON.stringify({ database_url: uri, region: `us-east-1`, + team_id: teamId, }), }) diff --git a/examples/nextjs/app/shape-proxy/route.ts b/examples/nextjs/app/shape-proxy/route.ts index aadfae4d3a..2973eb598a 100644 --- a/examples/nextjs/app/shape-proxy/route.ts +++ b/examples/nextjs/app/shape-proxy/route.ts @@ -11,7 +11,7 @@ export async function GET(request: Request) { }) if (process.env.DATABASE_ID) { - originUrl.searchParams.set(`database_id`, process.env.DATABASE_ID) + originUrl.searchParams.set(`source_id`, process.env.DATABASE_ID) } const headers = new Headers() diff --git a/examples/nextjs/sst.config.ts b/examples/nextjs/sst.config.ts index a77237e859..d3c93eab5f 100644 --- a/examples/nextjs/sst.config.ts +++ b/examples/nextjs/sst.config.ts @@ -34,17 +34,18 @@ export default $config({ ownerName: `neondb_owner`, }) - const databaseUri = getNeonDbUri(project, db) + const pooledDatabaseUri = getNeonDbUri(project, db, true) + const databaseUri = getNeonDbUri(project, db, false) try { - databaseUri.apply(applyMigrations) + pooledDatabaseUri.apply(applyMigrations) const electricInfo = databaseUri.apply((uri) => addDatabaseToElectric(uri) ) - const website = deployNextJsExample(electricInfo, databaseUri) + const website = deployNextJsExample(electricInfo, pooledDatabaseUri) return { - databaseUri, + pooledDatabaseUri, database_id: electricInfo.id, electric_token: electricInfo.token, website: website.url, @@ -84,28 +85,43 @@ function deployNextJsExample( function getNeonDbUri( project: $util.Output, - db: neon.Database + db: neon.Database, + pooled: boolean ) { const passwordOutput = neon.getBranchRolePasswordOutput({ projectId: project.id, branchId: project.defaultBranchId, roleName: db.ownerName, }) + 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}@${project.databaseHost}/${db.name}?sslmode=require` + return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${db.name}?sslmode=require` } async function addDatabaseToElectric( uri: string ): Promise<{ id: string; token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID - const result = await fetch(`${adminApi}/v1/databases`, { + const result = await fetch(`${adminApi}/v1/sources`, { method: `PUT`, headers: { "Content-Type": `application/json` }, body: JSON.stringify({ database_url: uri, region: `us-east-1`, + team_id: teamId, }), }) diff --git a/examples/proxy-auth/app/shape-proxy/route.ts b/examples/proxy-auth/app/shape-proxy/route.ts index 71cdb8e45a..bad1c8ad14 100644 --- a/examples/proxy-auth/app/shape-proxy/route.ts +++ b/examples/proxy-auth/app/shape-proxy/route.ts @@ -9,7 +9,7 @@ export async function GET(request: Request) { }) if (process.env.DATABASE_ID) { - originUrl.searchParams.set(`database_id`, process.env.DATABASE_ID) + originUrl.searchParams.set(`source_id`, process.env.DATABASE_ID) } if (process.env.ELECTRIC_TOKEN) { diff --git a/examples/proxy-auth/sst.config.ts b/examples/proxy-auth/sst.config.ts index b8f95eb246..4ec678cd79 100644 --- a/examples/proxy-auth/sst.config.ts +++ b/examples/proxy-auth/sst.config.ts @@ -89,13 +89,15 @@ async function addDatabaseToElectric( token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API - const url = new URL(`/v1/databases`, adminApi) + const teamId = process.env.ELECTRIC_TEAM_ID + const url = new URL(`/v1/sources`, adminApi) const result = await fetch(url, { method: `PUT`, headers: { "Content-Type": `application/json` }, body: JSON.stringify({ database_url: uri, region, + team_id: teamId, }), }) if (!result.ok) { diff --git a/examples/todo-app/src/routes/index.tsx b/examples/todo-app/src/routes/index.tsx index 114ff31a49..d0c2969d72 100644 --- a/examples/todo-app/src/routes/index.tsx +++ b/examples/todo-app/src/routes/index.tsx @@ -26,7 +26,7 @@ export default function Index() { url: new URL(`${import.meta.env.VITE_ELECTRIC_URL}/v1/shape/`).href, params: { table: `todos`, - database_id: import.meta.env.VITE_ELECTRIC_DATABASE_ID, + source_id: import.meta.env.VITE_ELECTRIC_DATABASE_ID, token: import.meta.env.VITE_ELECTRIC_TOKEN, }, }) diff --git a/examples/todo-app/sst.config.ts b/examples/todo-app/sst.config.ts index f3bdfea36a..685d8bcf8a 100644 --- a/examples/todo-app/sst.config.ts +++ b/examples/todo-app/sst.config.ts @@ -151,13 +151,15 @@ async function addDatabaseToElectric( uri: string ): Promise<{ id: string; token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID - const result = await fetch(`${adminApi}/v1/databases`, { + const result = await fetch(`${adminApi}/v1/sources`, { method: `PUT`, headers: { "Content-Type": `application/json` }, body: JSON.stringify({ database_url: uri, region: `us-east-1`, + team_id: teamId, }), }) diff --git a/examples/write-patterns/shared/app/config.ts b/examples/write-patterns/shared/app/config.ts index 731e8fe6a1..fec011bd24 100644 --- a/examples/write-patterns/shared/app/config.ts +++ b/examples/write-patterns/shared/app/config.ts @@ -1,11 +1,11 @@ export const ELECTRIC_URL = import.meta.env.VITE_ELECTRIC_URL || 'http://localhost:3000' -export const envParams: { database_id?: string; token?: string } = +export const envParams: { source_id?: string; token?: string } = import.meta.env.VITE_ELECTRIC_TOKEN && import.meta.env.VITE_ELECTRIC_DATABASE_ID ? { - database_id: import.meta.env.VITE_ELECTRIC_DATABASE_ID, + source_id: import.meta.env.VITE_ELECTRIC_DATABASE_ID, token: import.meta.env.VITE_ELECTRIC_TOKEN, } : {} diff --git a/examples/write-patterns/sst.config.ts b/examples/write-patterns/sst.config.ts index 90ff2b76e5..a634844e05 100644 --- a/examples/write-patterns/sst.config.ts +++ b/examples/write-patterns/sst.config.ts @@ -160,13 +160,15 @@ async function addDatabaseToElectric( region: 'us-east-1' | 'eu-west-1' = 'us-east-1' ): Promise<{ id: string; token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID - const result = await fetch(new URL('v1/databases', adminApi), { + const result = await fetch(new URL('v1/sources', adminApi), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ database_url, region, + team_id: teamId, }), }) diff --git a/examples/yjs/app/shape-proxy/[...table]/route.ts b/examples/yjs/app/shape-proxy/[...table]/route.ts index d389389091..da73a8e55b 100644 --- a/examples/yjs/app/shape-proxy/[...table]/route.ts +++ b/examples/yjs/app/shape-proxy/[...table]/route.ts @@ -11,7 +11,7 @@ export async function GET(request: Request) { }) if (process.env.DATABASE_ID) { - originUrl.searchParams.set(`database_id`, process.env.DATABASE_ID) + originUrl.searchParams.set(`source_id`, process.env.DATABASE_ID) } const headers = new Headers() diff --git a/examples/yjs/sst.config.ts b/examples/yjs/sst.config.ts index f4d9bfc3bb..ddd7a2465f 100644 --- a/examples/yjs/sst.config.ts +++ b/examples/yjs/sst.config.ts @@ -152,13 +152,15 @@ async function addDatabaseToElectric( uri: string ): Promise<{ id: string; token: string }> { const adminApi = process.env.ELECTRIC_ADMIN_API + const teamId = process.env.ELECTRIC_TEAM_ID - const result = await fetch(`${adminApi}/v1/databases`, { + const result = await fetch(`${adminApi}/v1/sources`, { method: `PUT`, headers: { "Content-Type": `application/json` }, body: JSON.stringify({ database_url: uri, region: `us-east-1`, + team_id: teamId, }), }) diff --git a/website/docs/api/clients/typescript.md b/website/docs/api/clients/typescript.md index 3360da1316..e7d7d17b3f 100644 --- a/website/docs/api/clients/typescript.md +++ b/website/docs/api/clients/typescript.md @@ -201,7 +201,7 @@ Note that certain parameter names are reserved for Electric's internal use and c - `handle` - `live` - `cursor` -- `database_id` +- `source_id` The following PostgreSQL-specific parameters should be included within the `params` object: - `table` - The root table for the shape diff --git a/website/electric-api.yaml b/website/electric-api.yaml index 5b7c1afdd0..c4ef1239ce 100644 --- a/website/electric-api.yaml +++ b/website/electric-api.yaml @@ -47,7 +47,7 @@ paths: using a `.` delimiter, such as `foo.issues`. If you don't provide a schema prefix, then the table is assumed to be in the `public.` schema. # Query parameters - - name: database_id + - name: source_id in: query schema: type: string @@ -426,7 +426,7 @@ paths: Can be qualified by the schema name. # Query parameters - - name: database_id + - name: source_id in: query schema: type: string @@ -450,87 +450,3 @@ paths: description: Bad request. "404": description: Database or shape not found (or shape deletion is not enabled). - /v1/admin/database: - post: - summary: Add Database - description: |- - Adds a database to Electric. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - database_url - - database_id - properties: - database_url: - type: string - description: PostgreSQL connection URL for the database - database_use_ipv6: - type: boolean - default: false - description: Whether to use IPv6 for database connections - database_id: - type: string - description: Unique identifier for the database (auto-generated UUID if not provided) - responses: - "200": - description: Database successfully added - content: - application/json: - schema: - type: string - description: The database ID of the added database - "400": - description: Bad request - content: - application/json: - schema: - type: string - description: Error message - examples: - already_exists: - value: "Database already exists." - db_in_use: - value: "The database localhost:54321/db is already in use by another tenant." - /v1/admin/database/{database_id}: - delete: - summary: Remove Database - description: |- - Removes a database from Electric. - parameters: - - name: database_id - in: path - required: true - schema: - type: string - description: The ID of the database to remove - responses: - "200": - description: Database successfully removed - content: - application/json: - schema: - type: string - description: The ID of the removed database - "400": - description: Bad request - content: - application/json: - schema: - type: object - properties: - database_id: - type: array - items: - type: string - description: Validation error messages - "404": - description: Database not found - content: - application/json: - schema: - type: string - example: "Database {id} not found."