diff --git a/package.json b/package.json index 8b09b26..6b3c518 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,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/prisma/schema.prisma b/prisma/schema.prisma index a87c091..b118875 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -2,111 +2,126 @@ // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { - provider = "prisma-client-js" - previewFeatures = ["fullTextSearch"] + 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 String @id @default(cuid()) // do not generate id + name String + title String + description String //make this 40 characters long and about a dog + dateUploaded DateTime? // make this + likes Like[] +} + +model Like { + id String @id @default(cuid()) // do not generate id + postId String + post Post @relation(fields: [postId], references: [id]) } // 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 VerifiedEmails { - email String @id + email String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - @@unique([email]) + @@unique([email]) } 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[] - branches Branch[] - projects Project[] + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + branches Branch[] + projects Project[] } model VerificationToken { - identifier String - token String @unique - expires DateTime + identifier String + token String @unique + expires DateTime - @@unique([identifier, token]) + @@unique([identifier, token]) } model Project { - 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]) + 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]) } model RDSInstance { - id String @id @default(cuid()) - baseConnection String? - project Project[] + id String @id @default(cuid()) + baseConnection String? + project Project[] } model Branch { - id String @id @default(cuid()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + 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: [projectRepository], references: [repository], onDelete: Cascade, onUpdate: Cascade) - projectRepository 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([projectRepository, name]) + @@unique([projectRepository, name]) } diff --git a/src/app/_components/DashboardContents.tsx b/src/app/_components/DashboardContents.tsx index 1443c07..ab20e02 100644 --- a/src/app/_components/DashboardContents.tsx +++ b/src/app/_components/DashboardContents.tsx @@ -4,7 +4,6 @@ import { api } from "~/trpc/react"; import { useState } from "react"; import ProjectList from "./ProjectList"; import SearchInput from "./SearchInput"; -import Link from "next/link"; export default function DashboardItems() { const [searchTerm, setSearchTerm] = useState(""); @@ -22,17 +21,13 @@ export default function DashboardItems() { searchTerms: searchTerm, }); - const nukeMutation = api.database.nuke.useMutation(); - const mappedProjects = projectsData?.map((project) => { - console.log(project.rdsInstanceId); - return { projectName: project.repository, route: project.repository, branchesCount: project.branches.length, databasesCount: project.branches.length, - instanceStatus: project.status, + instanceStatus: "Unknown", branches: project.branches.map((branch) => { return { creator: branch.createdBy.name ?? "Unknown", @@ -49,22 +44,14 @@ export default function DashboardItems() { return (
-
- - -
+ {isLoading &&
Loading...
} {error &&
Error: {error.message}
} {!isLoading && !error && (!mappedProjects || mappedProjects.length === 0) && (
- No available projects.{" "} - - Create a new project? - + No available projects
)} {mappedProjects && ( 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/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 1f39164..e183347 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -3,6 +3,7 @@ import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; import { greeting } from "./routers/greeting"; import { project } from "./routers/project"; import { githubWebhookRouter } from "./routers/prisma"; +import { dummy } from "./routers/dummy"; /** * This is the primary router for your server. @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({ health: greeting, database: project, webhook: githubWebhookRouter, + dummy: dummy, }); // export type definition of API diff --git a/src/server/api/routers/dummy.ts b/src/server/api/routers/dummy.ts new file mode 100644 index 0000000..e1d288d --- /dev/null +++ b/src/server/api/routers/dummy.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +import dummyCreate from "~/server/dummyData"; +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; + +export const dummy = createTRPCRouter({ + testDummy: publicProcedure + .input(z.object({ name: z.string() })) + .mutation(async () => { + const results = await dummyCreate(); + + return results; + }), +}); diff --git a/src/server/dummyData.ts b/src/server/dummyData.ts new file mode 100644 index 0000000..9c71842 --- /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 ?? "empty", + organization: process.env.OPENAI_ORG_ID ?? "empty", +}); + +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;