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 ( +
+ Search Icon + 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 ( +
+
+
+
+ Logo +

+ 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 ( -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full px-4 py-2 text-black" - /> - -
- ); -} 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;