From 3e68af8f7a2d8a501658a19903a76440a14fd5a1 Mon Sep 17 00:00:00 2001 From: ngc2207 Date: Sun, 10 Nov 2024 18:09:49 +0800 Subject: [PATCH] feat: add Prisma integration and initial data seeding --- .gitignore | 5 +- package.json | 11 +- prisma/schema.prisma | 165 +++++++++++++ prisma/seed.ts | 574 +++++++++++++++++++++++++++++++++++++++++++ src/lib/prisma.ts | 5 + 5 files changed, 755 insertions(+), 5 deletions(-) create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts create mode 100644 src/lib/prisma.ts diff --git a/.gitignore b/.gitignore index 67860b9..5e2ec1a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ next-env.d.ts cache # bun -bun.lockb \ No newline at end of file +bun.lockb + +# prisma +/prisma/migrations \ No newline at end of file diff --git a/package.json b/package.json index b47c685..1e81afc 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@prisma/client": "^5.22.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.456.0", @@ -19,13 +20,15 @@ "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5", + "@faker-js/faker": "^9.2.0", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "postcss": "^8", - "tailwindcss": "^3.4.1", "eslint": "^8", - "eslint-config-next": "15.0.3" + "eslint-config-next": "15.0.3", + "postcss": "^8", + "prisma": "^5.22.0", + "tailwindcss": "^3.4.1", + "typescript": "^5" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..1538dfa --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,165 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +enum UserRole { + ADMIN + TEACHER + STUDENT + GUEST +} + +model User { + id String @id @default(cuid()) + email String @unique @db.VarChar(255) + displayName String? @db.VarChar(255) + hashedPassword String @default("") + passwordSalt String @default("") + role UserRole @default(GUEST) + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + organizedEvents Event[] @relation("OrganizedEvents") + participatedEvents Event[] @relation("ParticipatedEvents") + createdQuestions Question[] @relation("CreatedQuestions") + submissions Submission[] +} + +enum EventType { + COMPETITION + EXAM + HOMEWORK +} + +model Event { + id String @id @default(cuid()) + name String + startTime DateTime @db.Timestamptz + endTime DateTime @db.Timestamptz + isVisible Boolean @default(true) + type EventType @default(COMPETITION) + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + organizer User[] @relation("OrganizedEvents") + participants User[] @relation("ParticipatedEvents") + eventQuestions EventQuestion[] +} + +enum QuestionDifficulty { + EASY + MEDIUM + HARD +} + +model Question { + id String @id @default(cuid()) + name String + statement String @db.Text + difficulty QuestionDifficulty @default(EASY) + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + creators User[] @relation("CreatedQuestions") + codeTemplates CodeTemplate[] + eventQuestions EventQuestion[] + tags QuestionTag[] @relation("QuestionTags") +} + +model QuestionTag { + id String @id @default(cuid()) + name String + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + questions Question[] @relation("QuestionTags") +} + +enum CodeLanguage { + C + CPP + JAVA +} + +model CodeTemplate { + id String @id @default(cuid()) + language CodeLanguage + starterCode String @db.Text + correctSolution String @default("") @db.Text + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + question Question @relation(fields: [questionId], references: [id]) + questionId String +} + +model EventQuestion { + id String @id @default(cuid()) + event Event @relation(fields: [eventId], references: [id]) + eventId String + question Question @relation(fields: [questionId], references: [id]) + questionId String + displayOrder Int @default(0) + hidden Boolean @default(false) + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + submissions Submission[] + + @@unique([eventId, questionId]) +} + +enum SubmissionStatus { + PENDING + ACCEPTED + REJECTED + COMPILATION_ERROR + RUNTIME_ERROR + INTERNAL_ERROR + PARTIAL_ACCEPTED + SKIPPED +} + +model Submission { + id String @id @default(cuid()) + submitter User @relation(fields: [submitterId], references: [id]) + submitterId String @db.VarChar(255) + eventQuestion EventQuestion @relation(fields: [eventQuestionId], references: [id]) + eventQuestionId String @db.VarChar(255) + language CodeLanguage + code String @db.Text + status SubmissionStatus + statusMessage String? + memoryUsage Int? + executionTime Int? + score Int? @default(0) + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz + testResults TestResult[] +} + +enum TestResultStatus { + ACCEPTED + REJECTED + TIMEOUT + MEMORY_LIMIT_EXCEEDED + COMPILATION_ERROR + RUNTIME_ERROR + INTERNAL_ERROR + OUTPUT_MISMATCH + SKIPPED +} + +model TestResult { + id String @id @default(cuid()) + submission Submission @relation(fields: [submissionId], references: [id]) + submissionId String + input String @db.Text + output String @db.Text + status TestResultStatus + statusMessage String? + memoryUsage Int? + executionTime Int? + score Int? @default(0) + createdAt DateTime @default(now()) @db.Timestamptz + updatedAt DateTime @updatedAt @db.Timestamptz +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..d08d584 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,574 @@ +import { + UserRole, + User, + EventType, + Event, + QuestionDifficulty, + Question, + QuestionTag, + CodeLanguage, + CodeTemplate, + EventQuestion, +} from "@prisma/client"; +import prisma from "@/lib/prisma"; +import { faker } from "@faker-js/faker"; + +async function createUser(role: UserRole): Promise { + return prisma.user.create({ + data: { + email: faker.internet.email(), + displayName: faker.person.fullName(), + hashedPassword: faker.internet.password(), + passwordSalt: faker.string.alphanumeric(16), + role, + }, + }); +} + +async function createUsers( + rolesWithCount: { role: UserRole; count: number }[] +): Promise { + const users = await Promise.all( + rolesWithCount.flatMap(({ role, count }) => + Array.from({ length: count }, () => createUser(role)) + ) + ); + return users; +} + +async function createQuestionTags(): Promise { + const questionTags = [ + "Array", + "String", + "Hash Table", + "Dynamic Programming", + "Math", + "Sorting", + "Greedy", + "Depth-First Search", + "Database", + "Binary Search", + "Matrix", + "Tree", + "Breadth-First Search", + ]; + + await prisma.questionTag.createMany({ + data: questionTags.map((name) => ({ name })), + }); + + return prisma.questionTag.findMany(); +} + +async function createQuestions(): Promise { + const questions = [ + { + name: "Two Sum", + statement: `Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.`, + difficulty: QuestionDifficulty.EASY, + tags: ["Array", "Hash Table"], + }, + { + name: "Add Two Numbers", + statement: `You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.`, + difficulty: QuestionDifficulty.MEDIUM, + tags: ["Linked List", "Math"], + }, + { + name: "Longest Substring Without Repeating Characters", + statement: `Given a string s, find the length of the longest substring without repeating characters.`, + difficulty: QuestionDifficulty.MEDIUM, + tags: ["Hash Table", "String", "Sliding Window"], + }, + ]; + + const creators = await prisma.user.findMany({ + where: { role: UserRole.TEACHER }, + }); + + if (creators.length === 0) { + throw new Error("No creators found for questions."); + } + + const tags = await prisma.questionTag.findMany(); + const tagMap = new Map(tags.map((tag) => [tag.name, tag])); + + const missingTags = questions.flatMap((question) => + question.tags.filter((tag) => !tagMap.has(tag)) + ); + + if (missingTags.length > 0) { + await prisma.questionTag.createMany({ + data: missingTags.map((name) => ({ name })), + }); + + const createdTags = await prisma.questionTag.findMany({ + where: { name: { in: missingTags } }, + }); + + createdTags.forEach((tag) => tagMap.set(tag.name, tag)); + } + + return Promise.all( + questions.map(({ name, statement, difficulty, tags: questionTags }) => + prisma.question.create({ + data: { + name, + statement, + difficulty, + tags: { + connect: questionTags.map((tag) => ({ id: tagMap.get(tag)!.id })), + }, + creators: { + connect: creators + .sort(() => Math.random() - 0.5) + .slice( + 0, + Math.max(1, Math.floor(Math.random() * creators.length)) + ) + .map((creator) => ({ id: creator.id })), + }, + }, + }) + ) + ); +} + +async function checkAndSeed( + check: () => Promise, + create: () => Promise, + entityName: string +): Promise { + const existingEntities = await check(); + if (existingEntities.length > 0) { + console.log(`${entityName} already exist, skipping seeding ${entityName}.`); + } else { + console.log(`Seeding ${entityName} ...`); + const entities = await create(); + console.log(`Created ${entityName}:`); + console.log(entities); + } +} + +async function createCodeTemplates(): Promise { + const codeTemplates = [ + [ + { + language: CodeLanguage.C, + starterCode: `/** + * Note: The returned array must be malloced, assume caller calls free(). + */ + int* twoSum(int* nums, int numsSize, int target, int* returnSize) { + + }`, + correctSolution: `int* twoSum(int* nums, int numsSize, int target, int* returnSize) { + struct hashTable { + int key; + int value; + UT_hash_handle hh; + } *hashTable = NULL, *item, *tmpItem; + + for (int i = 0; i < numsSize; i++) { + HASH_FIND_INT(hashTable, &nums[i], item); + if (item) { + int* result = malloc(sizeof(int) * 2); + result[0] = item->value; + result[1] = i; + *returnSize = 2; + HASH_ITER(hh, hashTable, item, tmpItem) { + HASH_DEL(hashTable, item); + free(item); + } + return result; + } + item = malloc(sizeof(struct hashTable)); + item->key = target - nums[i]; + item->value = i; + HASH_ADD_INT(hashTable, key, item); + } + HASH_ITER(hh, hashTable, item, tmpItem) { + HASH_DEL(hashTable, item); + free(item); + } + *returnSize = 0; + // If no valid pair is found, return an empty array + return malloc(sizeof(int) * 0); + }`, + }, + { + language: CodeLanguage.CPP, + starterCode: `class Solution { + public: + vector twoSum(vector& nums, int target) { + + } + };`, + correctSolution: `class Solution { + public: + vector twoSum(vector &nums, int target) { + unordered_map hash; + for (int i = 0; i < nums.size(); i++) { + hash[nums[i]] = i; + } + for (int i = 0; i < nums.size(); i++) { + int complement = target - nums[i]; + if (hash.find(complement) != hash.end() && hash[complement] != i) { + return {i, hash[complement]}; + } + } + // If no valid pair is found, return an empty vector + return {}; + } + };`, + }, + { + language: CodeLanguage.JAVA, + starterCode: `class Solution { + public int[] twoSum(int[] nums, int target) { + + } + }`, + correctSolution: `class Solution { + public int[] twoSum(int[] nums, int target) { + Map map = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + map.put(nums[i], i); + } + for (int i = 0; i < nums.length; i++) { + int complement = target - nums[i]; + if (map.containsKey(complement) && map.get(complement) != i) { + return new int[] { i, map.get(complement) }; + } + } + // In case there is no solution, return an empty array + return new int[] {}; + } + }`, + }, + ], + [ + { + language: CodeLanguage.C, + starterCode: `/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * struct ListNode *next; + * }; + */ + struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) { + + }`, + correctSolution: `struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) { + struct ListNode* dummyHead = malloc(sizeof(struct ListNode)); + dummyHead->val = 0; + dummyHead->next = NULL; + struct ListNode* curr = dummyHead; + int carry = 0; + while (l1 != NULL || l2 != NULL || carry != 0) { + int x = (l1 != NULL) ? l1->val : 0; + int y = (l2 != NULL) ? l2->val : 0; + int sum = carry + x + y; + carry = sum / 10; + curr->next = malloc(sizeof(struct ListNode)); + curr->next->val = sum % 10; + curr->next->next = NULL; + curr = curr->next; + if (l1 != NULL) l1 = l1->next; + if (l2 != NULL) l2 = l2->next; + } + struct ListNode* result = dummyHead->next; + free(dummyHead); // Free the memory allocated for dummyHead + return result; + }`, + }, + { + language: CodeLanguage.CPP, + starterCode: `/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * ListNode *next; + * ListNode() : val(0), next(nullptr) {} + * ListNode(int x) : val(x), next(nullptr) {} + * ListNode(int x, ListNode *next) : val(x), next(next) {} + * }; + */ + class Solution { + public: + ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { + + } + };`, + correctSolution: `class Solution { + public: + ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { + ListNode* dummyHead = new ListNode(0); + ListNode* curr = dummyHead; + int carry = 0; + while (l1 != NULL || l2 != NULL || carry != 0) { + int x = l1 ? l1->val : 0; + int y = l2 ? l2->val : 0; + int sum = carry + x + y; + carry = sum / 10; + curr->next = new ListNode(sum % 10); + curr = curr->next; + l1 = l1 ? l1->next : nullptr; + l2 = l2 ? l2->next : nullptr; + } + ListNode* result = dummyHead->next; + delete dummyHead; // Freeing the memory allocated for dummyHead + return result; + } + };`, + }, + { + language: CodeLanguage.JAVA, + starterCode: `/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ + class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + + } + }`, + correctSolution: `class Solution { + // Add Two Numbers (Java improved) + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode dummyHead = new ListNode(0); + ListNode curr = dummyHead; + int carry = 0; + while (l1 != null || l2 != null || carry != 0) { + int x = (l1 != null) ? l1.val : 0; + int y = (l2 != null) ? l2.val : 0; + int sum = carry + x + y; + carry = sum / 10; + curr.next = new ListNode(sum % 10); + curr = curr.next; + if (l1 != null) l1 = l1.next; + if (l2 != null) l2 = l2.next; + } + return dummyHead.next; + } + }`, + }, + ], + [ + { + language: CodeLanguage.C, + starterCode: `int lengthOfLongestSubstring(char* s) { + + }`, + correctSolution: `int lengthOfLongestSubstring(char * s){ + + int arr[256] = {0}; + int start = 0; + int len = 0; + + for (int i = 0; s[i]; i++) + { + char c = s[i]; + arr[c] != 0 ? (arr[c] > start ? start = arr[c] : start) : arr[c]; + (i - start + 1) > len ? len = (i - start + 1) : len; + arr[c] = i + 1; + } + return (len); + return(len); + }`, + }, + { + language: CodeLanguage.CPP, + starterCode: `class Solution { + public: + int lengthOfLongestSubstring(string s) { + + } + };`, + correctSolution: `class Solution { + public: + int lengthOfLongestSubstring(std::string_view sv) + { + std::vector vec; + std::size_t size = 0; + + for (auto c : sv) { + if (const auto& it = std::find(std::cbegin(vec), std::cend(vec), c); it != std::cend(vec)) { + size = std::max(size, vec.size()); + vec.erase(std::cbegin(vec), std::next(it)); + } + + vec.emplace_back(c); + } + + return std::max(size, vec.size()); + } + };`, + }, + { + language: CodeLanguage.JAVA, + starterCode: `class Solution { + public int lengthOfLongestSubstring(String s) { + + } + }`, + correctSolution: `class Solution { + public int lengthOfLongestSubstring(String s) { + Queue queue = new LinkedList<>(); + int res = 0; + for (char c : s.toCharArray()) { + while (queue.contains(c)) { + queue.poll(); + } + queue.offer(c); + res = Math.max(res, queue.size()); + } + return res; + } + }`, + }, + ], + ]; + + const questions = await prisma.question.findMany(); + const newCodeTemplates = questions.flatMap((question, index) => + codeTemplates[index].map((template) => ({ + ...template, + questionId: question.id, + })) + ); + + await prisma.codeTemplate.createMany({ + data: newCodeTemplates, + }); + + return prisma.codeTemplate.findMany(); +} + +async function createEvents(): Promise { + const users = await prisma.user.findMany(); + if (users.length === 0) { + throw new Error("No users found fororganizers and participants."); + } + + const eventTypes = Object.values(EventType); + + const events = await Promise.all( + Array.from({ length: 5 }, () => { + const startTime = faker.date.future(); + const endTime = faker.date.future({ refDate: startTime }); + const type = faker.helpers.arrayElement(eventTypes); + const isVisible = faker.datatype.boolean(); + + return prisma.event.create({ + data: { + name: faker.lorem.words(3), + startTime, + endTime, + type, + isVisible, + organizer: { + connect: { + id: faker.helpers.arrayElement(users).id, + }, + }, + participants: { + connect: faker.helpers + .arrayElements(users, { min: 1, max: 5 }) + .map((user) => ({ id: user.id })), + }, + }, + }); + }) + ); + + return events; +} + +async function createEventQuestions( + event: Event, + questions: Question[] +): Promise { + const selectedQuestions = faker.helpers.arrayElements( + questions, + faker.number.int({ min: 1, max: questions.length }) + ); + + const shuffledQuestions = faker.helpers.shuffle(selectedQuestions); + + return Promise.all( + shuffledQuestions.map((question, index) => + prisma.eventQuestion.create({ + data: { + event: { + connect: { + id: event.id, + }, + }, + question: { + connect: { + id: question.id, + }, + }, + displayOrder: index, + }, + }) + ) + ); +} + +async function main() { + const rolesWithCount = [ + { role: UserRole.ADMIN, count: 2 }, + { role: UserRole.TEACHER, count: 3 }, + { role: UserRole.STUDENT, count: 5 }, + { role: UserRole.GUEST, count: 2 }, + ]; + + try { + await checkAndSeed( + () => prisma.user.findMany(), + () => createUsers(rolesWithCount), + "users" + ); + + await checkAndSeed( + () => prisma.questionTag.findMany(), + createQuestionTags, + "question tags" + ); + + await checkAndSeed( + () => prisma.question.findMany(), + createQuestions, + "questions" + ); + + await checkAndSeed( + () => prisma.codeTemplate.findMany(), + createCodeTemplates, + "code templates" + ); + + await checkAndSeed(() => prisma.event.findMany(), createEvents, "events"); + + const questions = await prisma.question.findMany(); + const events = await prisma.event.findMany(); + await Promise.all( + events.map((event) => createEventQuestions(event, questions)) + ); + } catch (e) { + console.error(e); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +main(); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..0ac3ae0 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma: PrismaClient = new PrismaClient(); + +export default prisma;