Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions claim-db-worker/lib/prismaSchemaEditor/schemaApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const API_URL = "https://create-db-schema-api-routes.vercel.app/api/schema";
// const API_URL = "http://localhost:4141/api/schema";

export const formatSchema = async (schema: string): Promise<string> => {
const response = await fetch(
"https://create-db-schema-api-routes.vercel.app/api/schema/format",
{
const response = await fetch(`${API_URL}/format`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -31,9 +32,7 @@ export const pushSchema = async (
details?: string;
requiresForceReset?: boolean;
}> => {
const response = await fetch(
"https://create-db-schema-api-routes.vercel.app/api/schema/push",
{
const response = await fetch(`${API_URL}/push`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -65,9 +64,7 @@ export const pullSchema = async (
isEmpty?: boolean;
message?: string;
}> => {
const response = await fetch(
"https://create-db-schema-api-routes.vercel.app/api/schema/pull",
{
const response = await fetch(`${API_URL}/pull`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -95,9 +92,7 @@ export const forcePushSchema = async (
schema: string,
connectionString: string
): Promise<void> => {
const response = await fetch(
"https://create-db-schema-api-routes.vercel.app/api/schema/push-force",
{
const response = await fetch(`${API_URL}/push-force`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
2 changes: 1 addition & 1 deletion schema-api-routes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"dependencies": {
"@hono/node-server": "^1.8.2",
"hono": "^4.9.4",
"prisma": "^5.10.0",
"prisma": "^7.2.0",
"vercel": "^46.0.2"
},
"devDependencies": {
Expand Down
10 changes: 9 additions & 1 deletion schema-api-routes/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
import formatRoute from "./routes/schema/format.js";
import pullRoute from "./routes/schema/pull.js";
Expand Down Expand Up @@ -41,7 +42,7 @@ app.use(
);

app.get("/", (c) => {
return c.json({ message: "Hello World" });
return c.json({ message: "This is the schema API routes server for the create-db web app" });
});

app.route("/api/schema/format", formatRoute);
Expand All @@ -50,3 +51,10 @@ app.route("/api/schema/push", pushRoute);
app.route("/api/schema/push-force", pushForceRoute);

export default app;

const port = 4141;
console.log(`Server is running on http://localhost:${port}`);
serve({
fetch: app.fetch,
port,
});
Comment on lines +55 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Server bootstrap may break Vercel serverless deployment.

The serve() call executes unconditionally when this module is imported. Since vercel is listed as a dependency and Vercel typically imports the app export for serverless functions, this will attempt to bind port 4141 in the serverless environment, likely causing deployment failures.

Consider conditionally starting the server only in local development:

Proposed fix
 export default app;

-const port = 4141;
-console.log(`Server is running on http://localhost:${port}`);
-serve({
-  fetch: app.fetch,
-  port,
-});
+if (process.env.NODE_ENV !== "production") {
+  const port = Number(process.env.PORT) || 4141;
+  console.log(`Server is running on http://localhost:${port}`);
+  serve({
+    fetch: app.fetch,
+    port,
+  });
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const port = 4141;
console.log(`Server is running on http://localhost:${port}`);
serve({
fetch: app.fetch,
port,
});
if (process.env.NODE_ENV !== "production") {
const port = Number(process.env.PORT) || 4141;
console.log(`Server is running on http://localhost:${port}`);
serve({
fetch: app.fetch,
port,
});
}
🤖 Prompt for AI Agents
In @schema-api-routes/src/index.ts around lines 55 - 60, The unconditional call
to serve(...) with port = 4141 will run on import and can break Vercel
serverless usage; wrap the console.log and serve(...) invocation in a runtime
check so the server only starts when executed locally (e.g., guard with if
(import.meta.main) { console.log(...); serve({ fetch: app.fetch, port }); } or,
if using Node-compatible env vars, check process.env.VERCEL !== '1' or NODE_ENV
=== 'development' before calling serve), leaving the exported app.fetch
untouched for serverless imports.

121 changes: 58 additions & 63 deletions schema-api-routes/src/routes/schema/pull.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Hono } from "hono";
import { writeFile, readFile, unlink } from "fs/promises";
import { spawn } from "child_process";
import { writeFile, readFile, rm, mkdir } from "fs/promises";
import { execSync } from "child_process";

const app = new Hono();

app.post("/", async (c) => {
const minimalSchema = `generator client {
const minimalSchema = `generator client {
provider = "prisma-client"
output = "../app/generated/client"
}
Expand All @@ -15,74 +13,69 @@ datasource db {
provider = "postgresql"
}`;

try {
const connectionString = c.req.header("X-Connection-String");
async function createPrismaWorkspace(
connectionString: string,
schema: string
): Promise<{ workDir: string; schemaPath: string; cleanup: () => Promise<void> }> {
const timestamp = Date.now();
const workDir = `/tmp/prisma-${timestamp}`;
const schemaPath = `${workDir}/schema.prisma`;
const configPath = `${workDir}/prisma.config.ts`;

if (!connectionString) {
return c.json(
{ error: "Connection string not provided in headers" },
400
);
}
await mkdir(workDir, { recursive: true });

await writeFile(schemaPath, schema);

const tempDir = "/tmp";
const schemaPath = `${tempDir}/schema-${Date.now()}.prisma`;
const envPath = `${tempDir}/.env-${Date.now()}`;
const configContent = `import { defineConfig } from "prisma/config";

try {
await writeFile(envPath, `DATABASE_URL="${connectionString}"`);
await writeFile(schemaPath, minimalSchema);
export default defineConfig({
schema: "./schema.prisma",
datasource: {
url: "${connectionString}",
},
});
`;
Comment on lines +29 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same injection vulnerability as in push-force.ts.

Apply the same JSON.stringify fix for safe connection string escaping.

Proposed fix
   const configContent = `import { defineConfig } from "prisma/config";

 export default defineConfig({
   schema: "./schema.prisma",
   datasource: {
-    url: "${connectionString}",
+    url: ${JSON.stringify(connectionString)},
   },
 });
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const configContent = `import { defineConfig } from "prisma/config";
try {
await writeFile(envPath, `DATABASE_URL="${connectionString}"`);
await writeFile(schemaPath, minimalSchema);
export default defineConfig({
schema: "./schema.prisma",
datasource: {
url: "${connectionString}",
},
});
`;
const configContent = `import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "./schema.prisma",
datasource: {
url: ${JSON.stringify(connectionString)},
},
});
`;
🤖 Prompt for AI Agents
In @schema-api-routes/src/routes/schema/pull.ts around lines 29 - 37, The
interpolated connection string in the template assigned to configContent is
vulnerable to injection; replace the raw "${connectionString}" interpolation
with a safely escaped form by using JSON.stringify(connectionString) so the url
field becomes url: ${JSON.stringify(connectionString)} (this mirrors the fix
used in push-force.ts and ensures proper quoting/escaping of the connection
string).

await writeFile(configPath, configContent);

return {
workDir,
schemaPath,
cleanup: async () => {
try {
const result = execSync(`npx prisma db pull --schema=${schemaPath}`, {
env: {
...process.env,
DATABASE_URL: connectionString,
npm_config_cache: "/tmp/.npm",
npm_config_prefix: "/tmp/.npm",
},
cwd: process.cwd(),
encoding: "utf8",
stdio: "pipe",
});

const stdout = result;
const stderr = "";
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);

if (errorMessage.includes("The introspected database was empty")) {
return c.json({
success: true,
schema: minimalSchema,
message: "Database is empty - showing minimal schema",
isEmpty: true,
});
}

throw error;
await rm(workDir, { recursive: true, force: true });
} catch (e) {
console.error("Cleanup error:", e);
}
},
};
}

app.post("/", async (c) => {
const connectionString = c.req.header("X-Connection-String");
if (!connectionString) {
return c.json({ error: "Connection string not provided in headers" }, 400);
}

const schemaContent = await readFile(schemaPath, "utf-8");
const workspace = await createPrismaWorkspace(connectionString, minimalSchema);

return c.json({
success: true,
schema: schemaContent,
message: "Schema pulled successfully",
});
} finally {
try {
await unlink(schemaPath);
await unlink(envPath);
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
}
try {
execSync("pnpx prisma db pull", {
cwd: workspace.workDir,
encoding: "utf8",
stdio: "pipe",
});

const schemaContent = await readFile(workspace.schemaPath, "utf-8");

return c.json({
success: true,
schema: schemaContent,
message: "Schema pulled successfully",
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const message = error instanceof Error ? error.message : String(error);

if (errorMessage.includes("The introspected database was empty")) {
if (message.includes("The introspected database was empty")) {
return c.json({
success: true,
schema: minimalSchema,
Expand All @@ -94,10 +87,12 @@ datasource db {
return c.json(
{
error: "Failed to pull schema",
details: errorMessage,
details: message,
},
500
);
} finally {
await workspace.cleanup();
}
});

Expand Down
110 changes: 57 additions & 53 deletions schema-api-routes/src/routes/schema/push-force.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,71 @@
import { Hono } from "hono";
import { writeFile, unlink } from "fs/promises";
import { spawn } from "child_process";
import { writeFile, rm, mkdir } from "fs/promises";
import { execSync } from "child_process";

const app = new Hono();

app.post("/", async (c) => {
try {
const body = await c.req.json();
const { schema } = body as {
schema: string;
};
async function createPrismaWorkspace(
connectionString: string,
schema: string
): Promise<{ workDir: string; cleanup: () => Promise<void> }> {
const timestamp = Date.now();
const workDir = `/tmp/prisma-${timestamp}`;
const schemaPath = `${workDir}/schema.prisma`;
const configPath = `${workDir}/prisma.config.ts`;

if (!schema) {
return c.json({ error: "Schema is required" }, 400);
}
await mkdir(workDir, { recursive: true });

const connectionString = c.req.header("X-Connection-String");
await writeFile(schemaPath, schema);

if (!connectionString) {
return c.json(
{ error: "Connection string not provided in headers" },
400
);
}
const configContent = `import { defineConfig } from "prisma/config";

const tempDir = "/tmp";
const schemaPath = `${tempDir}/schema-${Date.now()}.prisma`;
const envPath = `${tempDir}/.env-${Date.now()}`;

try {
await writeFile(schemaPath, schema);
await writeFile(envPath, `DATABASE_URL="${connectionString}"`);
export default defineConfig({
schema: "./schema.prisma",
datasource: {
url: "${connectionString}",
},
});
`;
Comment on lines +20 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential injection vulnerability in config template.

The connectionString is directly interpolated into the template literal. If the connection string contains backticks, ${, or unescaped quotes, it could break the config syntax or enable injection.

Proposed fix using JSON.stringify for safe escaping
   const configContent = `import { defineConfig } from "prisma/config";

 export default defineConfig({
   schema: "./schema.prisma",
   datasource: {
-    url: "${connectionString}",
+    url: ${JSON.stringify(connectionString)},
   },
 });
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const configContent = `import { defineConfig } from "prisma/config";
const tempDir = "/tmp";
const schemaPath = `${tempDir}/schema-${Date.now()}.prisma`;
const envPath = `${tempDir}/.env-${Date.now()}`;
try {
await writeFile(schemaPath, schema);
await writeFile(envPath, `DATABASE_URL="${connectionString}"`);
export default defineConfig({
schema: "./schema.prisma",
datasource: {
url: "${connectionString}",
},
});
`;
const configContent = `import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "./schema.prisma",
datasource: {
url: ${JSON.stringify(connectionString)},
},
});
`;
🤖 Prompt for AI Agents
In @schema-api-routes/src/routes/schema/push-force.ts around lines 20 - 28, The
configContent template directly interpolates connectionString which can
break/enable injection if it contains backticks, ${, or quotes; update the
construction of configContent (variable name: configContent) to inject a safely
escaped string instead of raw connectionString — e.g., serialize/escape
connectionString (use JSON.stringify(connectionString) or an equivalent escaping
function) when interpolating into the defineConfig template so the generated
config is syntactically safe.

await writeFile(configPath, configContent);

return {
workDir,
cleanup: async () => {
try {
const result = execSync(
`npx prisma db push --schema=${schemaPath} --accept-data-loss --force-reset`,
{
env: {
...process.env,
DATABASE_URL: connectionString,
npm_config_cache: "/tmp/.npm",
npm_config_prefix: "/tmp/.npm",
},
cwd: process.cwd(),
encoding: "utf8",
stdio: "pipe",
}
);
} catch (error) {
throw error;
await rm(workDir, { recursive: true, force: true });
} catch (e) {
console.error("Cleanup error:", e);
}
},
};
}
Comment on lines +7 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract createPrismaWorkspace to a shared utility.

This helper is duplicated across push.ts, pull.ts, and push-force.ts. Extracting it to a shared module (e.g., utils/prismaWorkspace.ts) would reduce maintenance burden and ensure consistent behavior.

🤖 Prompt for AI Agents
In @schema-api-routes/src/routes/schema/push-force.ts around lines 7 - 41, The
createPrismaWorkspace helper is duplicated; extract it into a shared module
(e.g., export async function createPrismaWorkspace(...) in
utils/prismaWorkspace.ts) and move the implementation (timestamped /tmp
directory, schemaPath/configPath, mkdir, writeFile, configContent, and cleanup
using rm with try/catch) into that file, ensuring you import the needed fs
promises (mkdir, writeFile, rm) and export the function; then replace the local
implementations in push.ts, pull.ts, and push-force.ts with an import {
createPrismaWorkspace } from "utils/prismaWorkspace" (adjust relative path) and
remove the duplicated code from those files so they all use the single shared
utility.


return c.json({
success: true,
message: "Schema pushed successfully with force reset",
});
} finally {
try {
await unlink(schemaPath);
await unlink(envPath);
} catch (cleanupError) {
console.error("Cleanup error:", cleanupError);
}
}
app.post("/", async (c) => {
const body = await c.req.json();
const { schema } = body as { schema: string };

if (!schema) {
return c.json({ error: "Schema is required" }, 400);
}

const connectionString = c.req.header("X-Connection-String");
if (!connectionString) {
return c.json({ error: "Connection string not provided in headers" }, 400);
}

const workspace = await createPrismaWorkspace(connectionString, schema);

try {
execSync("pnpx prisma db push --accept-data-loss --force-reset", {
cwd: workspace.workDir,
encoding: "utf8",
stdio: "pipe",
});

return c.json({
success: true,
message: "Schema pushed successfully with force reset",
});
} catch (error) {
return c.json(
{
Expand All @@ -72,6 +74,8 @@ app.post("/", async (c) => {
},
500
);
} finally {
await workspace.cleanup();
}
});

Expand Down
Loading
Loading