Files
moreminimore-vibe/src/ipc/handlers/neon_handlers.ts
Will Chen b0f08eaf15 Neon / portal template support (#713)
TODOs:
- [x] Do restart when checkout / restore if there is a DB
- [x] List all branches (branch id, name, date)
- [x] Allow checking out versions with no DB
- [x] safeguard to never delete main branches
- [x] create app hook for neon template
- [x] weird UX with connector on configure panel
- [x] tiny neon logo in connector
- [x] deploy to vercel
- [x] build forgot password page
- [x] what about email setup
- [x] lots of imgix errors
- [x] edit file - db snapshot
- [x] DYAD_DISABLE_DB_PUSH
- [ ] update portal doc
- [x] switch preview branch to be read-only endpoint
- [x] disable supabase sys prompt if neon is enabled
- [ ] https://payloadcms.com/docs/upload/storage-adapters
- [x] need to use main branch...

Phase 2?
- [x] generate DB migrations
2025-08-04 16:36:09 -07:00

236 lines
7.1 KiB
TypeScript

import log from "electron-log";
import { createTestOnlyLoggedHandler } from "./safe_handle";
import { handleNeonOAuthReturn } from "../../neon_admin/neon_return_handler";
import {
getNeonClient,
getNeonErrorMessage,
getNeonOrganizationId,
} from "../../neon_admin/neon_management_client";
import {
CreateNeonProjectParams,
NeonProject,
GetNeonProjectParams,
GetNeonProjectResponse,
NeonBranch,
} from "../ipc_types";
import { db } from "../../db";
import { apps } from "../../db/schema";
import { eq } from "drizzle-orm";
import { ipcMain } from "electron";
import { EndpointType } from "@neondatabase/api-client";
import { retryOnLocked } from "../utils/retryOnLocked";
export const logger = log.scope("neon_handlers");
const testOnlyHandle = createTestOnlyLoggedHandler(logger);
export function registerNeonHandlers() {
// Do not use log handler because there's sensitive data in the response
ipcMain.handle(
"neon:create-project",
async (
_,
{ name, appId }: CreateNeonProjectParams,
): Promise<NeonProject> => {
const neonClient = await getNeonClient();
logger.info(`Creating Neon project: ${name} for app ${appId}`);
try {
// Get the organization ID
const orgId = await getNeonOrganizationId();
// Create project with retry on locked errors
const response = await retryOnLocked(
() =>
neonClient.createProject({
project: {
name: name,
org_id: orgId,
},
}),
`Create project ${name} for app ${appId}`,
);
if (!response.data.project) {
throw new Error(
"Failed to create project: No project data returned.",
);
}
const project = response.data.project;
const developmentBranch = response.data.branch;
const previewBranchResponse = await retryOnLocked(
() =>
neonClient.createProjectBranch(project.id, {
endpoints: [{ type: EndpointType.ReadOnly }],
branch: {
name: "preview",
parent_id: developmentBranch.id,
},
}),
`Create preview branch for project ${project.id}`,
);
if (
!previewBranchResponse.data.branch ||
!previewBranchResponse.data.connection_uris
) {
throw new Error(
"Failed to create preview branch: No branch data returned.",
);
}
const previewBranch = previewBranchResponse.data.branch;
// Store project and branch info in the app's DB row
await db
.update(apps)
.set({
neonProjectId: project.id,
neonDevelopmentBranchId: developmentBranch.id,
neonPreviewBranchId: previewBranch.id,
})
.where(eq(apps.id, appId));
logger.info(
`Successfully created Neon project: ${project.id} and development branch: ${developmentBranch.id} for app ${appId}`,
);
return {
id: project.id,
name: project.name,
connectionString: response.data.connection_uris[0].connection_uri,
branchId: developmentBranch.id,
};
} catch (error: any) {
const errorMessage = getNeonErrorMessage(error);
const message = `Failed to create Neon project for app ${appId}: ${errorMessage}`;
logger.error(message);
throw new Error(message);
}
},
);
ipcMain.handle(
"neon:get-project",
async (
_,
{ appId }: GetNeonProjectParams,
): Promise<GetNeonProjectResponse> => {
logger.info(`Getting Neon project info for app ${appId}`);
try {
// Get the app from the database to find the neonProjectId and neonBranchId
const app = await db
.select()
.from(apps)
.where(eq(apps.id, appId))
.limit(1);
if (app.length === 0) {
throw new Error(`App with ID ${appId} not found`);
}
const appData = app[0];
if (!appData.neonProjectId) {
throw new Error(`No Neon project found for app ${appId}`);
}
const neonClient = await getNeonClient();
console.log("PROJECT ID", appData.neonProjectId);
// Get project info
const projectResponse = await neonClient.getProject(
appData.neonProjectId,
);
if (!projectResponse.data.project) {
throw new Error("Failed to get project: No project data returned.");
}
const project = projectResponse.data.project;
// Get list of branches
const branchesResponse = await neonClient.listProjectBranches({
projectId: appData.neonProjectId,
});
if (!branchesResponse.data.branches) {
throw new Error("Failed to get branches: No branch data returned.");
}
// Map branches to our format
const branches: NeonBranch[] = branchesResponse.data.branches.map(
(branch) => {
let type: "production" | "development" | "snapshot" | "preview";
if (branch.default) {
type = "production";
} else if (branch.id === appData.neonDevelopmentBranchId) {
type = "development";
} else if (branch.id === appData.neonPreviewBranchId) {
type = "preview";
} else {
type = "snapshot";
}
// Find parent branch name if parent_id exists
let parentBranchName: string | undefined;
if (branch.parent_id) {
const parentBranch = branchesResponse.data.branches?.find(
(b) => b.id === branch.parent_id,
);
parentBranchName = parentBranch?.name;
}
return {
type,
branchId: branch.id,
branchName: branch.name,
lastUpdated: branch.updated_at,
parentBranchId: branch.parent_id,
parentBranchName,
};
},
);
logger.info(
`Successfully retrieved Neon project info for app ${appId}`,
);
return {
projectId: project.id,
projectName: project.name,
orgId: project.org_id ?? "<unknown_org_id>",
branches,
};
} catch (error) {
logger.error(
`Failed to get Neon project info for app ${appId}:`,
error,
);
throw error;
}
},
);
testOnlyHandle("neon:fake-connect", async (event) => {
// Call handleNeonOAuthReturn with fake data
handleNeonOAuthReturn({
token: "fake-neon-access-token",
refreshToken: "fake-neon-refresh-token",
expiresIn: 3600, // 1 hour
});
logger.info("Called handleNeonOAuthReturn with fake data during testing.");
// Simulate the deep link event
event.sender.send("deep-link-received", {
type: "neon-oauth-return",
url: "https://oauth.dyad.sh/api/integrations/neon/login",
});
logger.info("Sent fake neon deep-link-received event during testing.");
});
}