diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1 @@
+{}
diff --git a/package.json b/package.json
index aa2baff..645bb44 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@types/inquirer": "^9.0.7",
"@types/parse-git-config": "^3.0.4",
"@types/parse-github-url": "^1.0.3",
+ "autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"execa": "^9.1.0",
"git-url-parse": "^14.0.0",
@@ -38,6 +39,7 @@
"next-auth": "^4.24.6",
"nextjs-cors": "^2.2.0",
"octokit": "^4.0.2",
+ "openai": "^4.47.3",
"parse-git-config": "^3.0.0",
"parse-github-url": "^1.0.2",
"react": "18.2.0",
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e033912..349ae7f 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -2,96 +2,111 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
- provider = "prisma-client-js"
+ provider = "prisma-client-js"
+ previewFeatures = ["fullTextSearch"]
}
datasource db {
- provider = "postgresql"
- // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
- // Further reading:
- // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
- // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
- url = env("DATABASE_URL")
+ provider = "postgresql"
+ // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
+ // Further reading:
+ // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
+ // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
+ url = env("DATABASE_URL")
}
model Post {
- id Int @id @default(autoincrement())
- name String
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- createdBy User @relation(fields: [createdById], references: [id])
- createdById String
-
- @@index([name])
+ id String @id @default(cuid()) // do not generate id
+ name String
+ title String
+ description String //make this about a dog
+ dateUploaded DateTime?
}
// Necessary for Next auth
model Account {
- id String @id @default(cuid())
- userId String
- type String
- provider String
- providerAccountId String
- refresh_token String? // @db.Text
- refresh_token_expires_in Int?
- access_token String? // @db.Text
- expires_at Int?
- token_type String?
- scope String?
- id_token String? // @db.Text
- session_state String?
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- @@unique([provider, providerAccountId])
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String? // @db.Text
+ refresh_token_expires_in Int?
+ access_token String? // @db.Text
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String? // @db.Text
+ session_state String?
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([provider, providerAccountId])
}
model Session {
- id String @id @default(cuid())
- sessionToken String @unique
- userId String
- expires DateTime
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
- id String @id @default(cuid())
- name String?
- email String? @unique
- emailVerified DateTime?
- image String?
- accounts Account[]
- sessions Session[]
- posts Post[]
- Database Database[]
- verified Boolean @default(false)
+ id String @id @default(cuid())
+ name String?
+ email String? @unique
+ emailVerified DateTime?
+ image String?
+ accounts Account[]
+ sessions Session[]
+ branches Branch[]
+ projects Project[]
+ verified Boolean @default(false)
}
model VerificationToken {
- identifier String
- token String @unique
- expires DateTime
+ identifier String
+ token String @unique
+ expires DateTime
- @@unique([identifier, token])
+ @@unique([identifier, token])
}
model Project {
- repo String
- instances Database[]
+ owner String
+ ownerName String
+ repository String
+ repositoryName String
+ branches Branch[]
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ createdById String
+ rdsInstance RDSInstance @relation(fields: [rdsInstanceId], references: [id])
+ rdsInstanceId String
+
+ @@unique([repository])
+ @@index([ownerName, repositoryName, owner, repository])
+}
- @@unique([repo])
+model RDSInstance {
+ id String @id @default(cuid())
+ baseConnection String?
+ project Project[]
}
-model Database {
- id String @id @default(cuid())
- branch String
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+model Branch {
+ id String @id @default(cuid())
+ name 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
+ createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ createdById String
+ project Project @relation(fields: [projectRepository], references: [repository], onDelete: Cascade, onUpdate: Cascade)
+ projectRepository String
- @@unique([projectRepo, branch])
+ @@unique([projectRepository, name])
}
diff --git a/public/favicon.ico b/public/favicon.ico
index 60c702a..5f05056 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/public/images/ChevronIcon.svg b/public/images/ChevronIcon.svg
new file mode 100644
index 0000000..7ff721d
--- /dev/null
+++ b/public/images/ChevronIcon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/public/images/DeleteIcon.svg b/public/images/DeleteIcon.svg
new file mode 100644
index 0000000..f54f70a
--- /dev/null
+++ b/public/images/DeleteIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/images/PauseIcon.svg b/public/images/PauseIcon.svg
new file mode 100644
index 0000000..a0af8e5
--- /dev/null
+++ b/public/images/PauseIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/images/PlayIcon.svg b/public/images/PlayIcon.svg
new file mode 100644
index 0000000..0b47fb6
--- /dev/null
+++ b/public/images/PlayIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/images/PlusIcon.svg b/public/images/PlusIcon.svg
new file mode 100644
index 0000000..4feb323
--- /dev/null
+++ b/public/images/PlusIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/images/SearchIcon.svg b/public/images/SearchIcon.svg
new file mode 100644
index 0000000..bfb248b
--- /dev/null
+++ b/public/images/SearchIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/images/SoftwareDark.svg b/public/images/SoftwareDark.svg
new file mode 100644
index 0000000..2538713
--- /dev/null
+++ b/public/images/SoftwareDark.svg
@@ -0,0 +1,14 @@
+
diff --git a/public/images/SoftwareLight.svg b/public/images/SoftwareLight.svg
new file mode 100644
index 0000000..55a76e4
--- /dev/null
+++ b/public/images/SoftwareLight.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/app/_components/BranchRow.tsx b/src/app/_components/BranchRow.tsx
new file mode 100644
index 0000000..8666c97
--- /dev/null
+++ b/src/app/_components/BranchRow.tsx
@@ -0,0 +1,30 @@
+import { CreateButton, DeleteButton } from "./Button";
+
+export default function BranchRow(props: {
+ creator: string;
+ name: string;
+ status: string;
+}) {
+ const actionLabel =
+ props.status === "No DB" ? "Create Database" : "Connect to Database";
+
+ return (
+
+
+ {props.creator} / {props.name}
+
+
+
+ {actionLabel}
+
+
+
+ {props.status === "No DB" ? (
+ console.log("Create DB")} />
+ ) : (
+ console.log("Delete DB")} />
+ )}
+
+
+ );
+}
diff --git a/src/app/_components/Button.tsx b/src/app/_components/Button.tsx
new file mode 100644
index 0000000..8d0e36e
--- /dev/null
+++ b/src/app/_components/Button.tsx
@@ -0,0 +1,55 @@
+import Link from "next/link";
+
+export default function Button(props: {
+ href: string;
+ text: string;
+ yellow?: boolean;
+ newTab?: boolean;
+}) {
+ return (
+
+
+
+ );
+}
+
+export function CreateButton(props: { onClick: () => void }) {
+ return (
+
+ );
+}
+
+export function DeleteButton(props: { onClick: () => void }) {
+ return (
+
+ );
+}
+
+export function PauseButton(props: { onClick: () => void }) {
+ return (
+
+ );
+}
+
+export function PlayButton(props: { onClick: () => void }) {
+ return (
+
+ );
+}
diff --git a/src/app/_components/Dashboard.tsx b/src/app/_components/Dashboard.tsx
new file mode 100644
index 0000000..78dbd66
--- /dev/null
+++ b/src/app/_components/Dashboard.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import Navbar from "./NavBar";
+import DashboardItems from "./DashboardContents";
+
+const Dashboard: React.FC = () => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default Dashboard;
diff --git a/src/app/_components/DashboardContents.tsx b/src/app/_components/DashboardContents.tsx
new file mode 100644
index 0000000..ab20e02
--- /dev/null
+++ b/src/app/_components/DashboardContents.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { api } from "~/trpc/react";
+import { useState } from "react";
+import ProjectList from "./ProjectList";
+import SearchInput from "./SearchInput";
+
+export default function DashboardItems() {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [openProject, setOpenProject] = useState(null);
+
+ const handleToggle = (index: number) => {
+ setOpenProject(openProject === index ? null : index);
+ };
+
+ const {
+ data: projectsData,
+ error,
+ isLoading,
+ } = api.database.get.useQuery({
+ searchTerms: searchTerm,
+ });
+
+ const mappedProjects = projectsData?.map((project) => {
+ return {
+ projectName: project.repository,
+ route: project.repository,
+ branchesCount: project.branches.length,
+ databasesCount: project.branches.length,
+ instanceStatus: "Unknown",
+ branches: project.branches.map((branch) => {
+ return {
+ creator: branch.createdBy.name ?? "Unknown",
+ name: branch.name,
+ status: "TODO: REMOVE",
+ };
+ }),
+ creator: project.createdBy.name ?? "Unknown",
+ createdOn: project.createdAt,
+ };
+ });
+
+ console.log(mappedProjects);
+
+ return (
+
+
+ {isLoading &&
Loading...
}
+ {error &&
Error: {error.message}
}
+ {!isLoading &&
+ !error &&
+ (!mappedProjects || mappedProjects.length === 0) && (
+
+ No available projects
+
+ )}
+ {mappedProjects && (
+
+ )}
+
+ );
+}
diff --git a/src/app/_components/NavBar.tsx b/src/app/_components/NavBar.tsx
new file mode 100644
index 0000000..b8f4c45
--- /dev/null
+++ b/src/app/_components/NavBar.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import Link from "next/link";
+import Image from "next/image";
+import GenerateLogo from "../../../public/images/SoftwareLight.svg";
+
+const Navbar: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Navbar;
diff --git a/src/app/_components/ProjectCard.tsx b/src/app/_components/ProjectCard.tsx
new file mode 100644
index 0000000..31fb084
--- /dev/null
+++ b/src/app/_components/ProjectCard.tsx
@@ -0,0 +1,118 @@
+import React from "react";
+import BranchRow from "./BranchRow";
+import Link from "next/link";
+import { DeleteButton, PauseButton, PlayButton } from "./Button";
+import { api } from "~/trpc/react";
+
+interface Branch {
+ creator: string;
+ name: string;
+ status: string;
+}
+
+interface ProjectCardProps {
+ projectName: string;
+ route: string;
+ branchesCount: number;
+ databasesCount: number;
+ instanceStatus: string;
+ branches: Branch[];
+ creator: string;
+ createdOn: Date;
+ isOpen: boolean;
+ onToggle: () => void;
+}
+
+const ProjectCard: React.FC = ({
+ projectName,
+ route,
+ branchesCount,
+ databasesCount,
+ instanceStatus,
+ branches,
+ creator,
+ createdOn,
+ isOpen,
+ onToggle,
+}) => {
+ const pauseProjectMutation = api.database.stop.useMutation();
+ const startProjectMutation = api.database.start.useMutation();
+ const deleteProjectMutation = api.database.delete.useMutation();
+
+ const handlePause = () => {
+ pauseProjectMutation.mutate({ repoUrl: projectName });
+ };
+
+ const handleStart = () => {
+ startProjectMutation.mutate({ repoUrl: projectName });
+ };
+
+ const handleDelete = () => {
+ deleteProjectMutation.mutate({ repoUrl: projectName });
+ };
+ return (
+
+
+
+
+
+
+ {projectName.split("/").slice(-2)[0]}
+ {" "}
+ /{" "}
+
+ {route.split("/").slice(-1)[0]}
+ {" "}
+ - {branchesCount} branches - {databasesCount} databases
+
+
+
+ {instanceStatus === "Stopped" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {branches.map((branch, index) => (
+
+ ))}
+
+
+
Created by: {creator}
+
Created on: {createdOn.toString()}
+
+
+
+ );
+};
+
+export default ProjectCard;
diff --git a/src/app/_components/ProjectList.tsx b/src/app/_components/ProjectList.tsx
new file mode 100644
index 0000000..2a5f359
--- /dev/null
+++ b/src/app/_components/ProjectList.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import ProjectCard from "./ProjectCard";
+
+interface ProjectListProps {
+ projects: Array<{
+ projectName: string;
+ route: string;
+ branchesCount: number;
+ databasesCount: number;
+ instanceStatus: string;
+ branches: Array<{
+ creator: string;
+ name: string;
+ status: string;
+ }>;
+ creator: string;
+ createdOn: Date;
+ }>;
+ openProject: number | null;
+ handleToggle: (index: number) => void;
+}
+
+const ProjectList: React.FC = ({
+ projects,
+ openProject,
+ handleToggle,
+}) => {
+ return (
+
+ {projects.map((project, index) => (
+
handleToggle(index)}
+ />
+ ))}
+
+ );
+};
+
+export default ProjectList;
diff --git a/src/app/_components/SearchInput.tsx b/src/app/_components/SearchInput.tsx
new file mode 100644
index 0000000..55a652c
--- /dev/null
+++ b/src/app/_components/SearchInput.tsx
@@ -0,0 +1,35 @@
+"use client"; // Ensure this line is correct
+
+import React from "react";
+import Image from "next/image";
+import searchIcon from "../../../public/images/SearchIcon.svg";
+
+interface SearchInputProps {
+ searchTerm: string;
+ setSearchTerm: (term: string) => void;
+}
+
+const SearchInput: React.FC = ({
+ searchTerm,
+ setSearchTerm,
+}) => {
+ return (
+
+
+ setSearchTerm(e.target.value)}
+ className="p-2 min-w-[324px] w-full text-black placeholder:text-input-text outline-none"
+ />
+
+ );
+};
+
+export default SearchInput;
diff --git a/src/app/_components/SignInScreen.tsx b/src/app/_components/SignInScreen.tsx
new file mode 100644
index 0000000..a9ed63b
--- /dev/null
+++ b/src/app/_components/SignInScreen.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import Image from "next/image";
+import GenerateLogo from "../../../public/images/SoftwareLight.svg";
+import Button from "./Button";
+
+const SignInScreen: React.FC = () => {
+ return (
+
+
+
+
+
+
+ Generate{" "}
+ DevDB
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SignInScreen;
diff --git a/src/app/_components/create-post.tsx b/src/app/_components/create-post.tsx
deleted file mode 100644
index da3a1c8..0000000
--- a/src/app/_components/create-post.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-"use client";
-
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-
-import { api } from "~/trpc/react";
-
-export function CreatePost() {
- const router = useRouter();
- const [name, setName] = useState("");
-
- const createPost = api.post.create.useMutation({
- onSuccess: () => {
- router.refresh();
- setName("");
- },
- });
-
- return (
-
- );
-}
diff --git a/src/app/api/route.ts b/src/app/api/route.ts
deleted file mode 100644
index e4c6adb..0000000
--- a/src/app/api/route.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// app/api/routs.ts 👈🏽
-
-import { NextResponse } from "next/server";
-
-// To handle a GET request to /api
-export async function GET() {
- // Do whatever you want
- return NextResponse.json({ message: "Hello World" }, { status: 200 });
-}
-
-// To handle a POST request to /api
-export async function POST() {
- // Do whatever you want
- return NextResponse.json({ message: "Hello World" }, { status: 200 });
-}
-
-// Same logic to add a `PATCH`, `DELETE`...
diff --git a/src/app/dummy/page.tsx b/src/app/dummy/page.tsx
new file mode 100644
index 0000000..e445f1d
--- /dev/null
+++ b/src/app/dummy/page.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { useState } from "react";
+import { api } from "~/trpc/react";
+
+export default function Dummy() {
+ const [results, setResults] = useState("");
+ const testDummy = api.dummy.testDummy.useMutation();
+
+ const handleCreateDummyData = () => {
+ testDummy
+ .mutateAsync({ name: "test" })
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ .then((data) => setResults(`success: ${data.message}`))
+ .catch((error) => console.error(error));
+ };
+
+ return (
+
+
+ {results}
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2e96997..ebfb920 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,12 +1,13 @@
import "~/styles/globals.css";
-import { Inter } from "next/font/google";
+import { Space_Mono } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
-const inter = Inter({
+const space_mono = Space_Mono({
+ weight: ["400", "700"],
subsets: ["latin"],
- variable: "--font-sans",
+ variable: "--font-mono",
});
export const metadata = {
@@ -22,7 +23,7 @@ export default function RootLayout({
}) {
return (
-
+
{children}
diff --git a/src/app/new-project/page.tsx b/src/app/new-project/page.tsx
new file mode 100644
index 0000000..04831c9
--- /dev/null
+++ b/src/app/new-project/page.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import React, { useState } from "react";
+import Navbar from "../_components/NavBar";
+import { api } from "~/trpc/react";
+import { DBProvider } from "~/server/external/types";
+
+const CreateProject: React.FC = () => {
+ const [projectName, setProjectName] = useState("");
+ const [feedbackMessage, setFeedbackMessage] = useState(null);
+ const createProjectMutation = api.database.create.useMutation({
+ onSuccess: () => {
+ setFeedbackMessage("Project created successfully!");
+ },
+ onError: () => {
+ setFeedbackMessage("Failed to create project.");
+ },
+ });
+
+ const handleCreateProject = () => {
+ // Handle project creation logic here
+ console.log("Creating project:", projectName);
+ setFeedbackMessage("Creating project....");
+ createProjectMutation.mutate({
+ repoUrl: projectName,
+ provider: DBProvider.PostgreSQL,
+ });
+ };
+
+ return (
+ <>
+
+
+
Create New Project
+
+ setProjectName(e.target.value)}
+ className="px-12 py-3 bg-white text-gray-900 text-black placeholder:text-input-text"
+ />
+
+
+ {feedbackMessage && (
+
{feedbackMessage}
+ )}
+
+ >
+ );
+};
+
+export default CreateProject;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index b9fdc2d..f0f26ee 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,79 +1,8 @@
-import Link from "next/link";
-
import { getServerAuthSession } from "~/server/auth";
-import { api } from "~/trpc/server";
+import SignInScreen from "./_components/SignInScreen";
+import Dashboard from "./_components/Dashboard";
-export default async function Home() {
- const hello = await api.post.hello({ text: "from tRPC" });
+export default async function HomePage() {
const session = await getServerAuthSession();
-
- return (
-
-
-
- Generate Routes
-
-
-
-
First Steps →
-
- Just the basics - Everything you need to know to set up your
- database and authentication.
-
-
-
-
Documentation →
-
- Learn more about Create T3 App, the libraries it uses, and how to
- deploy it.
-
-
-
-
-
- {hello ? hello.greeting : "Loading tRPC query..."}
-
-
-
-
- {session && Logged in as {session.user?.name}}
-
-
- {session ? "Sign out" : "Sign in"}
-
-
-
-
-
-
-
- );
-}
-
-async function CrudShowcase() {
- const session = await getServerAuthSession();
- if (!session?.user) {
- return null;
- } else if (!session?.user.verified)
- return Not Yet Verified
;
-
- const sessionResponse = await api.post.getSessionToken();
-
- return (
-
- Your Generate Token:
-
{sessionResponse?.sessionToken}
-
- );
+ return <>{session ? : }>;
}
diff --git a/src/env.js b/src/env.js
index 0e27d60..ea01269 100644
--- a/src/env.js
+++ b/src/env.js
@@ -29,6 +29,8 @@ export const env = createEnv({
AWS_ACCESS_KEY_ID: z.string(),
AWS_SECRET_ACCESS_KEY: z.string(),
AWS_SESSION_TOKEN: z.string().optional(),
+ OPENAI_API_KEY: z.string(),
+ OPENAI_ORG_ID: z.string(),
},
/**
@@ -56,6 +58,8 @@ export const env = createEnv({
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN,
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
+ OPENAI_ORG_ID: process.env.OPENAI_ORG_ID,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index 32ac5a9..8598d12 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -1,9 +1,9 @@
-import { postRouter } from "~/server/api/routers/post";
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";
+import { dummy } from "./routers/dummy";
/**
* This is the primary router for your server.
@@ -11,11 +11,11 @@ import { githubWebhookRouter } from "./routers/prisma";
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
- post: postRouter,
github: gitHubRouter,
greeting: greeting,
database: database,
webhook: githubWebhookRouter,
+ dummy: dummy,
});
// export type definition of API
diff --git a/src/server/api/routers/databases.ts b/src/server/api/routers/databases.ts
index ec5f55c..9890eac 100644
--- a/src/server/api/routers/databases.ts
+++ b/src/server/api/routers/databases.ts
@@ -1,10 +1,51 @@
+import gitUrlParse from "git-url-parse";
import { z } from "zod";
import { protectedProcedure } from "~/server/api/trpc";
-import { CreateDatabase, GetDatabaseConnection } from "~/server/external/aws";
+import {
+ CreateDatabase,
+ DeleteDatabase,
+ GetDatabaseConnection,
+ StartDatabase,
+ StopDatabase,
+} from "~/server/external/aws";
import { DBProvider } from "~/server/external/types";
export const database = {
+ get: protectedProcedure
+ .input(z.object({ searchTerms: z.string() }))
+ .query(async ({ ctx, input }) => {
+ const searchResults = ctx.db.project.findMany({
+ include: {
+ createdBy: {
+ select: {
+ name: true,
+ },
+ },
+ branches: {
+ include: {
+ createdBy: {
+ select: {
+ name: true,
+ },
+ },
+ },
+ },
+ },
+ where: {
+ ...(input.searchTerms !== ""
+ ? {
+ repositoryName: {
+ search: input.searchTerms,
+ },
+ }
+ : {}),
+ },
+ });
+
+ return searchResults;
+ }),
+
create: protectedProcedure
.input(
z.object({ repoUrl: z.string(), provider: z.nativeEnum(DBProvider) }),
@@ -12,28 +53,59 @@ export const database = {
.mutation(async ({ ctx, input }) => {
const branch = "main";
- const projectCount = await ctx.db.project.count({
+ const parsedUrl = gitUrlParse(input.repoUrl);
+
+ const { owner, name, href } = parsedUrl;
+
+ const { id } = await ctx.db.rDSInstance.create({ data: {} });
+
+ await ctx.db.project.create({
+ data: {
+ owner: href.split("/").slice(0, -1).join("/"),
+ ownerName: owner,
+ repository: href,
+ repositoryName: name,
+ createdById: ctx.session.user.id,
+ rdsInstanceId: id,
+ },
+ });
+
+ await ctx.db.branch.create({
+ data: {
+ name: branch,
+ projectRepository: input.repoUrl,
+ createdById: ctx.session.user.id,
+ },
+ });
+
+ const result = await CreateDatabase(id, input.provider);
+
+ return result;
+ }),
+
+ delete: protectedProcedure
+ .input(z.object({ repoUrl: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ const findResults = await ctx.db.project.findFirstOrThrow({
+ select: {
+ rdsInstanceId: true,
+ },
where: {
- repo: input.repoUrl,
+ repository: input.repoUrl,
},
});
- if (projectCount == 0) {
- await ctx.db.project.create({
- data: { repo: input.repoUrl },
- });
- }
+ console.log(findResults);
- const newDb = await ctx.db.database.create({
- data: {
- branch: branch,
- projectRepo: input.repoUrl,
+ const deleteResults = await ctx.db.project.delete({
+ where: {
+ repository: input.repoUrl,
},
});
- const databaseName = newDb.id;
+ console.log(deleteResults);
- const result = await CreateDatabase(databaseName, input.provider);
+ const result = await DeleteDatabase(findResults.rdsInstanceId);
return result;
}),
@@ -41,18 +113,56 @@ export const database = {
endpoint: protectedProcedure
.input(z.object({ repoUrl: z.string() }))
.mutation(async ({ ctx, input }) => {
- const dbResults = await ctx.db.database.findFirstOrThrow({
+ const dbResults = await ctx.db.project.findFirstOrThrow({
select: {
- id: true,
+ rdsInstanceId: true,
},
where: {
- projectRepo: input.repoUrl,
+ repository: input.repoUrl,
},
});
- const result = await GetDatabaseConnection(dbResults.id);
+ console.log(dbResults);
+
+ const result = await GetDatabaseConnection(dbResults.rdsInstanceId);
return {
connection: result,
};
}),
+
+ start: protectedProcedure
+ .input(z.object({ repoUrl: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ const dbResults = await ctx.db.project.findFirstOrThrow({
+ select: {
+ rdsInstanceId: true,
+ },
+ where: {
+ repository: input.repoUrl,
+ },
+ });
+
+ console.log(dbResults);
+
+ const result = await StartDatabase(dbResults.rdsInstanceId);
+ return result;
+ }),
+
+ stop: protectedProcedure
+ .input(z.object({ repoUrl: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ const dbResults = await ctx.db.project.findFirstOrThrow({
+ select: {
+ rdsInstanceId: true,
+ },
+ where: {
+ repository: input.repoUrl,
+ },
+ });
+
+ console.log(dbResults);
+
+ const result = await StopDatabase(dbResults.rdsInstanceId);
+ return result;
+ }),
};
diff --git a/src/server/api/routers/dummy.ts b/src/server/api/routers/dummy.ts
new file mode 100644
index 0000000..18774b3
--- /dev/null
+++ b/src/server/api/routers/dummy.ts
@@ -0,0 +1,14 @@
+import { z } from "zod";
+
+import dummyCreate from "~/server/dummyData";
+import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
+
+export const dummy = createTRPCRouter({
+ testDummy: protectedProcedure
+ .input(z.object({ name: z.string() }))
+ .mutation(async () => {
+ const results = await dummyCreate();
+
+ return results;
+ }),
+});
diff --git a/src/server/api/routers/post.ts b/src/server/api/routers/post.ts
deleted file mode 100644
index 2d21c5c..0000000
--- a/src/server/api/routers/post.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { z } from "zod";
-
-import {
- createTRPCRouter,
- protectedProcedure,
- publicProcedure,
-} from "~/server/api/trpc";
-
-export const postRouter = createTRPCRouter({
- hello: publicProcedure
- .input(z.object({ text: z.string() }))
- .query(({ input }) => {
- return {
- greeting: `Hello ${input.text}`,
- };
- }),
-
- create: protectedProcedure
- .input(z.object({ name: z.string().min(1) }))
- .mutation(async ({ ctx, input }) => {
- // simulate a slow db call
- await new Promise((resolve) => setTimeout(resolve, 1000));
-
- return ctx.db.post.create({
- data: {
- name: input.name,
- createdBy: { connect: { id: ctx.session.user.id } },
- },
- });
- }),
-
- getSessionToken: protectedProcedure.query(({ ctx }) => {
- return ctx.db.session.findFirst({
- select: {
- sessionToken: true,
- },
- where: {
- userId: ctx.session.user.id,
- },
- });
- }),
-
- getSecretMessage: protectedProcedure.query(() => {
- return "you can now see this secret message!";
- }),
-});
diff --git a/src/server/dummyData.ts b/src/server/dummyData.ts
new file mode 100644
index 0000000..166b642
--- /dev/null
+++ b/src/server/dummyData.ts
@@ -0,0 +1,144 @@
+import { PrismaClient } from "@prisma/client";
+import OpenAI from "openai";
+import { readFileSync } from "fs";
+
+const prisma = new PrismaClient();
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_SECRET_KEY,
+ organization: process.env.OPENAI_ORG_ID,
+});
+
+interface ChatResponse {
+ choices: Choice[];
+}
+
+interface Choice {
+ message: {
+ content: string;
+ };
+}
+
+async function generateDummyData(
+ model: string,
+ fields: string[],
+ schema: string,
+): Promise {
+ console.log(
+ `Generating dummy data for model ${model} with fields: ${fields.join(", ")}`,
+ );
+ try {
+ const response: ChatResponse = (await openai.chat.completions.create({
+ model: "gpt-4o",
+ messages: [
+ {
+ role: "system",
+ content: `You are an expert data generator. Create a singular JSON object with realistic dummy data for a ${model} model with the following fields: ${fields.join(
+ ", ",
+ )}.
+ Guidelines:
+ - Please use this ${schema} to reference any relationships between models.
+ - Return strictly plain JSON that's not embedded, and without extraneous marks.
+ - Do not generate fields that are unique IDs.
+ - Each field should have a maximum of 20 characters per entry unless specified otherwise.
+ - DateTime fields should be in the format "YYYY-MM-DDTHH:MM:SSZ" (e.g., 2022-03-15T10:00:00Z).
+ - For fields with comments, follow the instructions in the comments.
+ - For nested fields, use the Prisma nested create syntax. If using createMany for a nested field,
+ ignore any of the parent model's fields that are not required.
+ - Ensure that related records are created with valid references or data.
+ Example syntax for nested fields:
+ {
+ "email": "saanvi@prisma.io",
+ "posts": {
+ "createMany": {
+ "data": [
+ { "title": "My first post" },
+ { "title": "My second post" }
+ ]
+ }
+ }
+ }
+ Return only the JSON object.`,
+ },
+ ],
+ max_tokens: 4096,
+ })) as ChatResponse;
+ console.log("Response", response.choices[0]?.message.content);
+ return response.choices[0]?.message.content;
+ } catch (error) {
+ console.error("Failed to generate dummy data:", error);
+ throw error;
+ }
+}
+
+async function tryCreateDataWithRetry(
+ modelName: string,
+ fields: string[],
+ schema: string,
+ retries: number,
+): Promise {
+ for (let attempt = 1; attempt <= retries; attempt++) {
+ try {
+ const dummyData: unknown = await generateDummyData(
+ modelName,
+ fields,
+ schema,
+ );
+ if (dummyData) {
+ // Use type assertion to safely access the model
+ const model = prisma[modelName as keyof typeof prisma];
+ if (model && prisma) {
+ const createCommand = prisma[
+ modelName.toLocaleLowerCase() as keyof typeof prisma
+ ] as { create: (data: unknown) => unknown };
+ await createCommand.create({
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ data: JSON.parse(dummyData as string),
+ });
+ console.log(`Data inserted for model ${modelName}`);
+ return; // Exit function if successful
+ } else {
+ console.error(`Model ${modelName} does not exist in Prisma Client.`);
+ return; // Exit function if model does not exist
+ }
+ }
+ } catch (error) {
+ console.error(`Attempt ${attempt} failed:`, error);
+ if (attempt < retries) {
+ console.log("Retrying...");
+ } else {
+ console.error("All attempts failed.");
+ throw error;
+ }
+ }
+ }
+}
+
+async function dummyCreate(): Promise<{ message: string }> {
+ console.log("Creating dummy data...");
+ const schema = readFileSync("./prisma/schema.prisma", "utf8");
+ const models = schema.match(/model \w+ {[^}]+}/g);
+ console.log(models);
+
+ if (models) {
+ for (const modelDef of models) {
+ console.log(modelDef);
+ const modelName = modelDef.match(/model (\w+) {/)?.[1];
+ console.log(modelName);
+ const fields = modelDef
+ .match(" {([^}]+)}")
+ ?.map((field) => field.slice(0, -1));
+
+ console.log(fields);
+
+ if (modelName && fields) {
+ await tryCreateDataWithRetry(modelName, fields, schema, 5);
+ }
+ }
+ }
+ await prisma.$disconnect();
+ return { message: "Dummy data created successfully." };
+}
+
+export default dummyCreate;
diff --git a/src/server/external/aws.ts b/src/server/external/aws.ts
index f29f233..73538d0 100644
--- a/src/server/external/aws.ts
+++ b/src/server/external/aws.ts
@@ -3,7 +3,17 @@ import {
CreateDBInstanceCommand,
DescribeDBInstancesCommand,
DBInstanceNotFoundFault,
+ DeleteDBInstanceCommand,
+ StartDBInstanceCommand,
+ StopDBInstanceCommand,
type CreateDBInstanceCommandOutput,
+ type DeleteDBInstanceCommandOutput,
+ type DeleteDBInstanceCommandInput,
+ type CreateDBInstanceCommandInput,
+ type StartDBInstanceCommandInput,
+ type StartDBInstanceResult,
+ type StopDBInstanceResult,
+ type StopDBInstanceCommandInput,
} from "@aws-sdk/client-rds";
import type { DBProvider } from "./types";
@@ -13,7 +23,7 @@ export async function CreateDatabase(
name: string,
provider: DBProvider,
): Promise {
- const commandInput = {
+ const commandInput: CreateDBInstanceCommandInput = {
DBName: "defaultdb",
AllocatedStorage: 20,
DBInstanceClass: "db.t3.micro",
@@ -28,6 +38,19 @@ export async function CreateDatabase(
return result;
}
+export async function DeleteDatabase(
+ name: string,
+): Promise {
+ const commandInput: DeleteDBInstanceCommandInput = {
+ DBInstanceIdentifier: name,
+ };
+
+ const command = new DeleteDBInstanceCommand(commandInput);
+ const result = client.send(command);
+
+ return result;
+}
+
export async function GetDatabaseConnection(
instanceId: string | undefined,
): Promise {
@@ -68,3 +91,27 @@ export async function GetDatabaseConnection(
}
}
}
+
+export async function StartDatabase(
+ name: string,
+): Promise {
+ const input: StartDBInstanceCommandInput = {
+ DBInstanceIdentifier: name,
+ };
+ const command = new StartDBInstanceCommand(input);
+ const result = await client.send(command);
+
+ return result;
+}
+
+export async function StopDatabase(
+ name: string,
+): Promise {
+ const input: StopDBInstanceCommandInput = {
+ DBInstanceIdentifier: name,
+ };
+ const command = new StopDBInstanceCommand(input);
+ const result = await client.send(command);
+
+ return result;
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index f06488f..d83b958 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,14 +1,34 @@
import { type Config } from "tailwindcss";
-import { fontFamily } from "tailwindcss/defaultTheme";
export default {
content: ["./src/**/*.tsx"],
theme: {
extend: {
fontFamily: {
- sans: ["var(--font-sans)", ...fontFamily.sans],
+ mono: ["var(--font-mono)"],
},
},
+ colors: {
+ transparent: "transparent",
+ current: "currentColor",
+ white: "#ffffff",
+ black: "#000000",
+ gray: "#EEEEEE",
+ generate: {
+ DEFAULT: "#187DFF",
+ dark: "#092A55",
+ sw: { light: "#ffdc88", DEFAULT: "#FFBF3C", dark: "#ffac20" },
+ },
+ project: {
+ row: "#EEEEEE",
+ },
+ input: {
+ text: "#999999",
+ },
+ },
+ gradientColorStopPositions: {
+ 33: "33%",
+ },
},
plugins: [],
} satisfies Config;