Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better AWS Endpoints #10

Merged
merged 4 commits into from
May 22, 2024
Merged
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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/node_modules
/.pnp
.pnp.js
bun.lockb
*.lockb

# testing
/coverage
Expand Down Expand Up @@ -42,4 +42,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo

bun.lockb
*.lockb
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,28 @@
"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",
"@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",
Expand Down
26 changes: 24 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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])
}
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({
github: gitHubRouter,
greeting: greeting,
database: database,
webhook: githubWebhookRouter,
});

// export type definition of API
Expand Down
78 changes: 56 additions & 22 deletions src/server/api/routers/databases.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,59 @@
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" });

export const database = publicProcedure
.input(z.object({ name: z.string() }))
.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);

return result;
});
import { CreateDatabase, GetDatabaseConnection } from "~/server/external/aws";
import { DBProvider } from "~/server/external/types";
import { TRPCError } from "@trpc/server";

export const database = {
create: publicProcedure
.input(
z.object({ repoUrl: z.string(), provider: z.nativeEnum(DBProvider) }),
)
.mutation(async ({ ctx, input }) => {
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 },
});
}

const newDb = await ctx.db.database.create({
data: {
branch: branch,
projectRepo: input.repoUrl,
},
});

const databaseName = newDb.id;

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 {
connection: result,
};
}),
};
63 changes: 63 additions & 0 deletions src/server/api/routers/prisma.ts
Original file line number Diff line number Diff line change
@@ -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 };
}),
});
70 changes: 70 additions & 0 deletions src/server/external/aws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
RDSClient,
CreateDBInstanceCommand,
DescribeDBInstancesCommand,
DBInstanceNotFoundFault,
type CreateDBInstanceCommandOutput,
} 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<CreateDBInstanceCommandOutput> {
const commandInput = {
DBName: "defaultdb",
AllocatedStorage: 20,
DBInstanceClass: "db.t3.micro",
DBInstanceIdentifier: name,
Engine: provider,
MasterUsername: "dev",
MasterUserPassword: "devpassword123",
};
const command = new CreateDBInstanceCommand(commandInput);
const result = client.send(command);

return result;
}

export async function GetDatabaseConnection(
instanceId: string | undefined,
): Promise<string | undefined> {
const input = {
// DescribeDBInstancesMessage
DBInstanceIdentifier: instanceId,
};
const command = new DescribeDBInstancesCommand(input);

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 {
throw Error(
`Database not yet available, current status: ${response.DBInstances[0]?.DBInstanceStatus}`,
);
}
}
} catch (error) {
if (error instanceof DBInstanceNotFoundFault) {
throw error;
} else {
throw error;
}
}
}
4 changes: 4 additions & 0 deletions src/server/external/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum DBProvider {
PostgreSQL = "postgres",
MySQL = "mysql",
}