Skip to content

Commit 429fe60

Browse files
committed
DB migration
1 parent 407d957 commit 429fe60

File tree

8 files changed

+271
-3
lines changed

8 files changed

+271
-3
lines changed

src/client_logic/template_hook.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ export async function neonTemplateHook({
2727
key: "PAYLOAD_SECRET",
2828
value: uuidv4(),
2929
},
30+
{
31+
key: "NEXT_PUBLIC_SERVER_URL",
32+
value: "http://localhost:32100",
33+
},
34+
{
35+
key: "GMAIL_USER",
36+
37+
},
38+
{
39+
key: "GOOGLE_APP_PASSWORD",
40+
value: "GENERATE AT https://myaccount.google.com/apppasswords",
41+
},
3042
],
3143
});
3244
console.log("App env vars set");

src/components/PortalMigrate.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useState } from "react";
2+
import { useMutation } from "@tanstack/react-query";
3+
import { IpcClient } from "@/ipc/ipc_client";
4+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5+
import { Button } from "@/components/ui/button";
6+
import { ExternalLink, Database, Loader2 } from "lucide-react";
7+
import { showSuccess, showError } from "@/lib/toast";
8+
import { useVersions } from "@/hooks/useVersions";
9+
10+
interface PortalMigrateProps {
11+
appId: number;
12+
}
13+
14+
export const PortalMigrate = ({ appId }: PortalMigrateProps) => {
15+
const [output, setOutput] = useState<string>("");
16+
const { refreshVersions } = useVersions(appId);
17+
18+
const migrateMutation = useMutation({
19+
mutationFn: async () => {
20+
const ipcClient = IpcClient.getInstance();
21+
return ipcClient.portalMigrateCreate({ appId });
22+
},
23+
onSuccess: (result) => {
24+
setOutput(result.output);
25+
showSuccess(
26+
"Database migration file generated and committed successfully!",
27+
);
28+
refreshVersions();
29+
},
30+
onError: (error) => {
31+
const errorMessage =
32+
error instanceof Error ? error.message : String(error);
33+
setOutput(`Error: ${errorMessage}`);
34+
showError(errorMessage);
35+
},
36+
});
37+
38+
const handleCreateMigration = () => {
39+
setOutput(""); // Clear previous output
40+
migrateMutation.mutate();
41+
};
42+
43+
const openDocs = () => {
44+
const ipcClient = IpcClient.getInstance();
45+
ipcClient.openExternalUrl(
46+
"https://www.dyad.sh/docs/templates/portal#create-a-database-migration",
47+
);
48+
};
49+
50+
return (
51+
<Card>
52+
<CardHeader className="pb-3">
53+
<CardTitle className="flex items-center gap-2">
54+
<Database className="w-5 h-5 text-primary" />
55+
Portal Database Migration
56+
</CardTitle>
57+
</CardHeader>
58+
<CardContent className="space-y-4">
59+
<p className="text-sm text-gray-600 dark:text-gray-400">
60+
Generate a new database migration file for your Portal app.
61+
</p>
62+
63+
<div className="flex items-center gap-3">
64+
<Button
65+
onClick={handleCreateMigration}
66+
disabled={migrateMutation.isPending}
67+
// className="bg-primary hover:bg-purple-700 text-white"
68+
>
69+
{migrateMutation.isPending ? (
70+
<>
71+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
72+
Generating...
73+
</>
74+
) : (
75+
<>
76+
<Database className="w-4 h-4 mr-2" />
77+
Generate database migration
78+
</>
79+
)}
80+
</Button>
81+
82+
<Button
83+
variant="outline"
84+
size="sm"
85+
onClick={openDocs}
86+
className="text-sm"
87+
>
88+
<ExternalLink className="w-3 h-3 mr-1" />
89+
Docs
90+
</Button>
91+
</div>
92+
93+
{output && (
94+
<div className="mt-4">
95+
<div className="bg-gray-50 dark:bg-gray-900 border rounded-lg p-3">
96+
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
97+
Command Output:
98+
</h4>
99+
<div className="max-h-64 overflow-auto">
100+
<pre className="text-xs text-gray-600 dark:text-gray-400 whitespace-pre-wrap font-mono">
101+
{output}
102+
</pre>
103+
</div>
104+
</div>
105+
</div>
106+
)}
107+
</CardContent>
108+
</Card>
109+
);
110+
};

src/components/preview_panel/PublishPanel.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
33
import { useLoadApp } from "@/hooks/useLoadApp";
44
import { GitHubConnector } from "@/components/GitHubConnector";
55
import { VercelConnector } from "@/components/VercelConnector";
6+
import { PortalMigrate } from "@/components/PortalMigrate";
67
import { IpcClient } from "@/ipc/ipc_client";
78
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
89

@@ -78,6 +79,9 @@ export const PublishPanel = () => {
7879
</h1>
7980
</div>
8081

82+
{/* Portal Section - Show only if app has neon project */}
83+
{app.neonProjectId && <PortalMigrate appId={selectedAppId} />}
84+
8185
{/* GitHub Section */}
8286
<Card>
8387
<CardHeader className="pb-3">

src/ipc/handlers/portal_handlers.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { createLoggedHandler } from "./safe_handle";
2+
import log from "electron-log";
3+
import { db } from "../../db";
4+
import { apps } from "../../db/schema";
5+
import { eq } from "drizzle-orm";
6+
import { getDyadAppPath } from "../../paths/paths";
7+
import { spawn } from "child_process";
8+
import fs from "node:fs";
9+
import git from "isomorphic-git";
10+
import { gitCommit } from "../utils/git_utils";
11+
import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils";
12+
13+
const logger = log.scope("portal_handlers");
14+
const handle = createLoggedHandler(logger);
15+
16+
async function getApp(appId: number) {
17+
const app = await db.query.apps.findFirst({
18+
where: eq(apps.id, appId),
19+
});
20+
if (!app) {
21+
throw new Error(`App with id ${appId} not found`);
22+
}
23+
return app;
24+
}
25+
26+
export function registerPortalHandlers() {
27+
handle(
28+
"portal:migrate-create",
29+
async (_, { appId }: { appId: number }): Promise<{ output: string }> => {
30+
const app = await getApp(appId);
31+
const appPath = getDyadAppPath(app.path);
32+
33+
// Run the migration command
34+
const migrationOutput = await new Promise<string>((resolve, reject) => {
35+
logger.info(`Running migrate:create for app ${appId} at ${appPath}`);
36+
37+
const process = spawn("npm run migrate:create -- --skip-empty", {
38+
cwd: appPath,
39+
shell: true,
40+
stdio: "pipe",
41+
});
42+
43+
let stdout = "";
44+
let stderr = "";
45+
46+
process.stdout?.on("data", (data) => {
47+
const output = data.toString();
48+
stdout += output;
49+
logger.info(`migrate:create stdout: ${output}`);
50+
});
51+
52+
process.stderr?.on("data", (data) => {
53+
const output = data.toString();
54+
stderr += output;
55+
logger.warn(`migrate:create stderr: ${output}`);
56+
});
57+
58+
process.on("close", (code) => {
59+
const combinedOutput =
60+
stdout + (stderr ? `\n\nErrors/Warnings:\n${stderr}` : "");
61+
62+
if (code === 0) {
63+
if (stdout.includes("Migration created at")) {
64+
logger.info(
65+
`migrate:create completed successfully for app ${appId}`,
66+
);
67+
} else {
68+
logger.error(
69+
`migrate:create completed successfully for app ${appId} but no migration was created`,
70+
);
71+
reject(
72+
new Error(
73+
"No migration was created because no changes were found.",
74+
),
75+
);
76+
}
77+
resolve(combinedOutput);
78+
} else {
79+
logger.error(
80+
`migrate:create failed for app ${appId} with exit code ${code}`,
81+
);
82+
const errorMessage = `Migration creation failed (exit code ${code})\n\n${combinedOutput}`;
83+
reject(new Error(errorMessage));
84+
}
85+
});
86+
87+
process.on("error", (err) => {
88+
logger.error(`Failed to spawn migrate:create for app ${appId}:`, err);
89+
const errorMessage = `Failed to run migration command: ${err.message}\n\nOutput:\n${stdout}\n\nErrors:\n${stderr}`;
90+
reject(new Error(errorMessage));
91+
});
92+
});
93+
94+
if (app.neonProjectId && app.neonDevelopmentBranchId) {
95+
try {
96+
await storeDbTimestampAtCurrentVersion({
97+
appId: app.id,
98+
});
99+
} catch (error) {
100+
logger.error(
101+
"Error storing Neon timestamp at current version:",
102+
error,
103+
);
104+
throw new Error(
105+
"Could not store Neon timestamp at current version; database versioning functionality is not working: " +
106+
error,
107+
);
108+
}
109+
}
110+
111+
// Stage all changes and commit
112+
try {
113+
await git.add({
114+
fs,
115+
dir: appPath,
116+
filepath: ".",
117+
});
118+
119+
const commitHash = await gitCommit({
120+
path: appPath,
121+
message: "[dyad] Generate database migration file",
122+
});
123+
124+
logger.info(`Successfully committed migration changes: ${commitHash}`);
125+
return { output: migrationOutput };
126+
} catch (gitError) {
127+
logger.error(`Migration created but failed to commit: ${gitError}`);
128+
throw new Error(`Migration created but failed to commit: ${gitError}`);
129+
}
130+
},
131+
);
132+
}

src/ipc/ipc_client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,15 @@ export class IpcClient {
828828

829829
// --- End Neon Management ---
830830

831+
// --- Portal Management ---
832+
public async portalMigrateCreate(params: {
833+
appId: number;
834+
}): Promise<{ output: string }> {
835+
return this.ipcRenderer.invoke("portal:migrate-create", params);
836+
}
837+
838+
// --- End Portal Management ---
839+
831840
public async getSystemDebugInfo(): Promise<SystemDebugInfo> {
832841
return this.ipcRenderer.invoke("get-system-debug-info");
833842
}

src/ipc/ipc_host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
2727
import { registerProblemsHandlers } from "./handlers/problems_handlers";
2828
import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
2929
import { registerTemplateHandlers } from "./handlers/template_handlers";
30+
import { registerPortalHandlers } from "./handlers/portal_handlers";
3031

3132
export function registerIpcHandlers() {
3233
// Register all IPC handlers by category
@@ -59,4 +60,5 @@ export function registerIpcHandlers() {
5960
registerCapacitorHandlers();
6061
registerAppEnvVarsHandlers();
6162
registerTemplateHandlers();
63+
registerPortalHandlers();
6264
}

src/ipc/utils/neon_timestamp_utils.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ export async function storeDbTimestampAtCurrentVersion({
129129
return { timestamp: currentTimestamp };
130130
} catch (error) {
131131
logger.error("Error in storeDbTimestampAtCurrentVersion:", error);
132-
throw new Error(
133-
"Could not store DB timestamp at current version: " + error,
134-
);
132+
throw error;
135133
}
136134
}

src/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const validInvokeChannels = [
105105
"check-problems",
106106
"restart-dyad",
107107
"get-templates",
108+
"portal:migrate-create",
108109
// Test-only channels
109110
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
110111
// We can't detect with IS_TEST_BUILD in the preload script because

0 commit comments

Comments
 (0)