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
This commit is contained in:
235
src/ipc/handlers/neon_handlers.ts
Normal file
235
src/ipc/handlers/neon_handlers.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
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.");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user