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
236 lines
7.1 KiB
TypeScript
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.");
|
|
});
|
|
}
|