From 6f33a364f1782b23488086a083087df49be932d7 Mon Sep 17 00:00:00 2001 From: Nate Sawant Date: Tue, 21 May 2024 21:09:14 -0400 Subject: [PATCH 1/4] Get database endpoint --- src/server/api/routers/databases.ts | 20 ++----- src/server/external/aws.ts | 82 +++++++++++++++++++++++++++++ src/server/external/types.ts | 4 ++ 3 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 src/server/external/aws.ts create mode 100644 src/server/external/types.ts diff --git a/src/server/api/routers/databases.ts b/src/server/api/routers/databases.ts index 19bc68e..635f60a 100644 --- a/src/server/api/routers/databases.ts +++ b/src/server/api/routers/databases.ts @@ -1,25 +1,13 @@ import { z } from "zod"; -import { RDSClient, CreateDBInstanceCommand } from "@aws-sdk/client-rds"; - import { publicProcedure } from "~/server/api/trpc"; -import process from "process"; - -const client = new RDSClient({ region: "us-east-1" }); +import { CreateDatabase } from "~/server/external/aws"; +import { DBProvider } from "~/server/external/types"; export const database = publicProcedure - .input(z.object({ name: z.string() })) + .input(z.object({ name: z.string(), provider: z.nativeEnum(DBProvider) })) .mutation(async ({ input }) => { - const commandInput = { - AllocatedStorage: 20, - DBInstanceClass: "db.t3.micro", - DBInstanceIdentifier: input.name, - Engine: "mysql", - MasterUserPassword: process.env.AWS_MASTER_PASSWORD, - MasterUsername: process.env.AWS_MASTER_USERNAME, - }; - const command = new CreateDBInstanceCommand(commandInput); - const result = await client.send(command); + const result = await CreateDatabase(input.name, input.provider); return result; }); diff --git a/src/server/external/aws.ts b/src/server/external/aws.ts new file mode 100644 index 0000000..2cc5793 --- /dev/null +++ b/src/server/external/aws.ts @@ -0,0 +1,82 @@ +import { + RDSClient, + CreateDBInstanceCommand, + DescribeDBInstancesCommand, + DBInstanceNotFoundFault, +} from "@aws-sdk/client-rds"; +import type { DBProvider } from "./types"; + +const client = new RDSClient({ region: "us-east-1" }); + +export async function CreateDatabase( + name: string, + provider: DBProvider, +): Promise { + const commandInput = { + AllocatedStorage: 20, + DBInstanceClass: "db.t3.micro", + DBInstanceIdentifier: name, + Engine: provider, + MasterUsername: "dev", + MasterUserPassword: "devpassword123", + }; + const command = new CreateDBInstanceCommand(commandInput); + const result = await client.send(command); + + try { + const endpoint = await GetDatabaseConnection( + result.DBInstance?.DBInstanceIdentifier, + ); + if (endpoint) return endpoint; + else throw Error("Problem getting connection detail."); + } catch (error) { + console.error(error); + throw error; + } +} + +async function GetDatabaseConnection( + instanceId: string | undefined, +): Promise { + const retries = 10; + const retryInterval = 120000; + const input = { + // DescribeDBInstancesMessage + DBInstanceIdentifier: instanceId, + }; + const command = new DescribeDBInstancesCommand(input); + + for (let i = 0; i < retries; i++) { + console.log(`Attempt #${i}`); + try { + const response = await client.send(command); + + if (response.DBInstances) { + if (response.DBInstances?.length < 1) { + throw Error("Database not found"); + } else if (response.DBInstances[0]?.DBInstanceStatus == "Available") { + const provider = response.DBInstances[0].Engine; + const username = "dev"; + const password = "devpassword123"; + const awsEndpoint = response.DBInstances[0].Endpoint?.Address; //"test-database.cw6wi7ttmo36.us-east-1.rds.amazonaws.com"; + const port = response.DBInstances[0].Endpoint?.Port; + const dbName = response.DBInstances[0].DBName; + + const connection = `${provider}://${username}:${password}@${awsEndpoint}:${port}/${dbName}`; + + return connection; + } else { + console.log("retrying..."); + await new Promise((resolve) => setTimeout(resolve, retryInterval)); + } + } + } catch (error) { + if (error instanceof DBInstanceNotFoundFault) { + throw error; + } else { + console.error(error); + // Sleep before retry + } + } + } +} diff --git a/src/server/external/types.ts b/src/server/external/types.ts new file mode 100644 index 0000000..b9b1520 --- /dev/null +++ b/src/server/external/types.ts @@ -0,0 +1,4 @@ +export enum DBProvider { + PostgreSQL = "postgres", + MySQL = "mysql", +} From 45e25a845c59fe211024929a0223b78a722afc1b Mon Sep 17 00:00:00 2001 From: Nate Sawant Date: Tue, 21 May 2024 23:22:21 -0400 Subject: [PATCH 2/4] Add AWS stuff --- package.json | 5 ++- prisma/schema.prisma | 26 ++++++++++- src/server/api/routers/databases.ts | 61 +++++++++++++++++++++++--- src/server/external/aws.ts | 68 ++++++++++++----------------- 4 files changed, 109 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 7a9cc5e..87c9ee9 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,21 @@ "@trpc/client": "next", "@trpc/react-query": "next", "@trpc/server": "next", + "@types/git-url-parse": "^9.0.3", "@types/inquirer": "^9.0.7", "@types/parse-git-config": "^3.0.4", + "@types/parse-github-url": "^1.0.3", "execa": "^9.1.0", + "git-url-parse": "^14.0.0", "husky": "latest", "inquirer": "^9.2.22", "install": "^0.13.0", "next": "^14.2.1", "next-auth": "^4.24.6", + "nextjs-cors": "^2.2.0", "octokit": "^4.0.2", "parse-git-config": "^3.0.0", "parse-github-url": "^1.0.2", - "nextjs-cors": "^2.2.0", "react": "18.2.0", "react-dom": "18.2.0", "server-only": "^0.0.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ff96560..5cd5f97 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,14 +55,15 @@ model Session { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] + Database Database[] } model VerificationToken { @@ -72,3 +73,24 @@ model VerificationToken { @@unique([identifier, token]) } + +model Project { + repo String + instances Database[] + + @@unique([repo]) +} + +model Database { + id String @id @default(cuid()) + branch String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + createdBy User? @relation(fields: [createdById], references: [id], onDelete: Cascade, onUpdate: Cascade) + createdById String? + project Project @relation(fields: [projectRepo], references: [repo], onDelete: Cascade, onUpdate: Cascade) + projectRepo String + + @@unique([projectRepo, branch]) +} diff --git a/src/server/api/routers/databases.ts b/src/server/api/routers/databases.ts index 635f60a..a4fcb8c 100644 --- a/src/server/api/routers/databases.ts +++ b/src/server/api/routers/databases.ts @@ -1,13 +1,60 @@ import { z } from "zod"; import { publicProcedure } from "~/server/api/trpc"; -import { CreateDatabase } from "~/server/external/aws"; +import { CreateDatabase, GetDatabaseConnection } from "~/server/external/aws"; import { DBProvider } from "~/server/external/types"; +import gitUrlParse from "git-url-parse"; -export const database = publicProcedure - .input(z.object({ name: z.string(), provider: z.nativeEnum(DBProvider) })) - .mutation(async ({ input }) => { - const result = await CreateDatabase(input.name, input.provider); +export const database = { + create: publicProcedure + .input( + z.object({ repoUrl: z.string(), provider: z.nativeEnum(DBProvider) }), + ) + .mutation(async ({ ctx, input }) => { + const parseResults = gitUrlParse(input.repoUrl); - return result; - }); + const branch = "main"; + + const projectCount = await ctx.db.project.count({ + where: { + repo: input.repoUrl, + }, + }); + + if (projectCount == 0) { + await ctx.db.project.create({ + data: { repo: input.repoUrl }, + }); + } + + await ctx.db.database.create({ + data: { + branch: branch, + projectRepo: input.repoUrl, + }, + }); + + const databaseName = `${parseResults.owner}-${parseResults.name}-${branch}`; + + const result = await CreateDatabase(databaseName, input.provider); + + return result; + }), + + endpoint: publicProcedure + .input(z.object({ repoURL: z.string() })) + .mutation(async ({ ctx, input }) => { + const dbResults = await ctx.db.database.findFirstOrThrow({ + select: { + id: true, + }, + where: { + projectRepo: input.repoURL, + }, + }); + + const result = await GetDatabaseConnection(dbResults.id); + + return result; + }), +}; diff --git a/src/server/external/aws.ts b/src/server/external/aws.ts index 2cc5793..cf3ea2c 100644 --- a/src/server/external/aws.ts +++ b/src/server/external/aws.ts @@ -3,6 +3,7 @@ import { CreateDBInstanceCommand, DescribeDBInstancesCommand, DBInstanceNotFoundFault, + type CreateDBInstanceCommandOutput, } from "@aws-sdk/client-rds"; import type { DBProvider } from "./types"; @@ -11,7 +12,7 @@ const client = new RDSClient({ region: "us-east-1" }); export async function CreateDatabase( name: string, provider: DBProvider, -): Promise { +): Promise { const commandInput = { AllocatedStorage: 20, DBInstanceClass: "db.t3.micro", @@ -21,62 +22,47 @@ export async function CreateDatabase( MasterUserPassword: "devpassword123", }; const command = new CreateDBInstanceCommand(commandInput); - const result = await client.send(command); + const result = client.send(command); - try { - const endpoint = await GetDatabaseConnection( - result.DBInstance?.DBInstanceIdentifier, - ); - if (endpoint) return endpoint; - else throw Error("Problem getting connection detail."); - } catch (error) { - console.error(error); - throw error; - } + return result; } -async function GetDatabaseConnection( +export async function GetDatabaseConnection( instanceId: string | undefined, ): Promise { - const retries = 10; - const retryInterval = 120000; const input = { // DescribeDBInstancesMessage DBInstanceIdentifier: instanceId, }; const command = new DescribeDBInstancesCommand(input); - for (let i = 0; i < retries; i++) { - console.log(`Attempt #${i}`); - try { - const response = await client.send(command); + try { + const response = await client.send(command); - if (response.DBInstances) { - if (response.DBInstances?.length < 1) { - throw Error("Database not found"); - } else if (response.DBInstances[0]?.DBInstanceStatus == "Available") { - const provider = response.DBInstances[0].Engine; - const username = "dev"; - const password = "devpassword123"; - const awsEndpoint = response.DBInstances[0].Endpoint?.Address; //"test-database.cw6wi7ttmo36.us-east-1.rds.amazonaws.com"; - const port = response.DBInstances[0].Endpoint?.Port; - const dbName = response.DBInstances[0].DBName; + if (response.DBInstances) { + if (response.DBInstances?.length < 1) { + throw Error("Database not found"); + } else if (response.DBInstances[0]?.DBInstanceStatus == "Available") { + const provider = response.DBInstances[0].Engine; + const username = "dev"; + const password = "devpassword123"; + const awsEndpoint = response.DBInstances[0].Endpoint?.Address; //"test-database.cw6wi7ttmo36.us-east-1.rds.amazonaws.com"; + const port = response.DBInstances[0].Endpoint?.Port; + const dbName = response.DBInstances[0].DBName; - const connection = `${provider}://${username}:${password}@${awsEndpoint}:${port}/${dbName}`; + const connection = `${provider}://${username}:${password}@${awsEndpoint}:${port}/${dbName}`; - return connection; - } else { - console.log("retrying..."); - await new Promise((resolve) => setTimeout(resolve, retryInterval)); - } - } - } catch (error) { - if (error instanceof DBInstanceNotFoundFault) { - throw error; + return connection; } else { - console.error(error); - // Sleep before retry + throw Error("Database not yet available"); } } + } catch (error) { + if (error instanceof DBInstanceNotFoundFault) { + throw error; + } else { + console.error(error); + // Sleep before retry + } } } From ce1519da8c3e834325b2910eb9ea2684a9f74847 Mon Sep 17 00:00:00 2001 From: Nate Sawant Date: Tue, 21 May 2024 23:42:26 -0400 Subject: [PATCH 3/4] Fix issues --- src/server/api/routers/databases.ts | 17 ++++++++--------- src/server/external/aws.ts | 10 ++++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/server/api/routers/databases.ts b/src/server/api/routers/databases.ts index a4fcb8c..2a4d3d0 100644 --- a/src/server/api/routers/databases.ts +++ b/src/server/api/routers/databases.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { publicProcedure } from "~/server/api/trpc"; import { CreateDatabase, GetDatabaseConnection } from "~/server/external/aws"; import { DBProvider } from "~/server/external/types"; -import gitUrlParse from "git-url-parse"; +import { TRPCError } from "@trpc/server"; export const database = { create: publicProcedure @@ -11,8 +11,6 @@ export const database = { z.object({ repoUrl: z.string(), provider: z.nativeEnum(DBProvider) }), ) .mutation(async ({ ctx, input }) => { - const parseResults = gitUrlParse(input.repoUrl); - const branch = "main"; const projectCount = await ctx.db.project.count({ @@ -27,14 +25,14 @@ export const database = { }); } - await ctx.db.database.create({ + const newDb = await ctx.db.database.create({ data: { branch: branch, projectRepo: input.repoUrl, }, }); - const databaseName = `${parseResults.owner}-${parseResults.name}-${branch}`; + const databaseName = newDb.id; const result = await CreateDatabase(databaseName, input.provider); @@ -42,19 +40,20 @@ export const database = { }), endpoint: publicProcedure - .input(z.object({ repoURL: z.string() })) + .input(z.object({ repoUrl: z.string() })) .mutation(async ({ ctx, input }) => { const dbResults = await ctx.db.database.findFirstOrThrow({ select: { id: true, }, where: { - projectRepo: input.repoURL, + projectRepo: input.repoUrl, }, }); const result = await GetDatabaseConnection(dbResults.id); - - return result; + return { + connection: result, + }; }), }; diff --git a/src/server/external/aws.ts b/src/server/external/aws.ts index cf3ea2c..f29f233 100644 --- a/src/server/external/aws.ts +++ b/src/server/external/aws.ts @@ -14,6 +14,7 @@ export async function CreateDatabase( provider: DBProvider, ): Promise { const commandInput = { + DBName: "defaultdb", AllocatedStorage: 20, DBInstanceClass: "db.t3.micro", DBInstanceIdentifier: name, @@ -42,7 +43,7 @@ export async function GetDatabaseConnection( if (response.DBInstances) { if (response.DBInstances?.length < 1) { throw Error("Database not found"); - } else if (response.DBInstances[0]?.DBInstanceStatus == "Available") { + } else if (response.DBInstances[0]?.DBInstanceStatus === "available") { const provider = response.DBInstances[0].Engine; const username = "dev"; const password = "devpassword123"; @@ -54,15 +55,16 @@ export async function GetDatabaseConnection( return connection; } else { - throw Error("Database not yet available"); + throw Error( + `Database not yet available, current status: ${response.DBInstances[0]?.DBInstanceStatus}`, + ); } } } catch (error) { if (error instanceof DBInstanceNotFoundFault) { throw error; } else { - console.error(error); - // Sleep before retry + throw error; } } } From a5a7f542369ffa58bb92c1a415810fbeddd50d3f Mon Sep 17 00:00:00 2001 From: Ansh P <39590072+patela22@users.noreply.github.com> Date: Mon, 20 May 2024 19:47:48 -0400 Subject: [PATCH 4/4] Updated the routes (#9) * Updated the routes * Pushing small changes * Um * Updated Libraries --- .gitignore | 4 +- package.json | 1 + src/server/api/root.ts | 2 + src/server/api/routers/prisma.ts | 63 ++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/server/api/routers/prisma.ts diff --git a/.gitignore b/.gitignore index a0768bd..1a35e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ /node_modules /.pnp .pnp.js -bun.lockb +*.lockb # testing /coverage @@ -42,4 +42,4 @@ yarn-error.log* # typescript *.tsbuildinfo -bun.lockb \ No newline at end of file +*.lockb \ No newline at end of file diff --git a/package.json b/package.json index 87c9ee9..eb0a369 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@auth/prisma-adapter": "^1.4.0", "@aws-sdk/client-rds": "^3.572.0", + "@octokit/rest": "^20.1.1", "@prisma/client": "^5.10.2", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "^5.25.0", diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 4a74563..32ac5a9 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -3,6 +3,7 @@ import { gitHubRouter } from "./routers/github-router"; import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; import { greeting } from "./routers/greeting"; import { database } from "./routers/databases"; +import { githubWebhookRouter } from "./routers/prisma"; /** * This is the primary router for your server. @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({ github: gitHubRouter, greeting: greeting, database: database, + webhook: githubWebhookRouter, }); // export type definition of API diff --git a/src/server/api/routers/prisma.ts b/src/server/api/routers/prisma.ts new file mode 100644 index 0000000..0627631 --- /dev/null +++ b/src/server/api/routers/prisma.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { Octokit } from "@octokit/rest"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { writeFile } from "fs/promises"; +const execAsync = promisify(exec); + +// Initialize Octokit with an access token +const octokit = new Octokit({ + auth: process.env.GITHUB_ACCESS_TOKEN, +}); + +export const githubWebhookRouter = createTRPCRouter({ + handlePush: publicProcedure + .input( + z.object({ + ref: z.string(), + after: z.string(), + repository: z.object({ + owner: z.string(), + name: z.string(), + }), + installation: z.object({ + id: z.number(), + }), + }), + ) + .mutation(async ({ input }) => { + if (input.ref === "refs/heads/main") { + try { + // Fetch the content of the Prisma schema file using Octokit + const response = await octokit.repos.getContent({ + owner: input.repository.owner, + repo: input.repository.name, + path: "prisma/schema.prisma", + mediaType: { + format: "raw", + }, + }); + + const schemaContent = response.data as unknown as string; + + // Save schemaContent to a local file + await writeFile("prisma/schema.prisma", schemaContent, "utf8"); + + // Apply changes using Prisma + const { stdout, stderr } = await execAsync("npx prisma db push"); + console.log(stdout); + if (stderr) { + console.error("Error during database push:", stderr); + throw new Error("Failed to update database schema"); + } + return { success: true }; + } catch (error) { + console.error("Failed to handle GitHub push:", error); + throw new Error("Failed to handle GitHub push"); + } + } + + return { ignored: true }; + }), +});