feat: add Prisma integration and initial data seeding

This commit is contained in:
ngc2207 2024-11-10 18:09:49 +08:00
parent 2fc607accd
commit 3e68af8f7a
5 changed files with 755 additions and 5 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ cache
# bun
bun.lockb
# prisma
/prisma/migrations

View File

@ -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"
}
}

165
prisma/schema.prisma Normal file
View File

@ -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
}

574
prisma/seed.ts Normal file
View File

@ -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<User> {
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<User[]> {
const users = await Promise.all(
rolesWithCount.flatMap(({ role, count }) =>
Array.from({ length: count }, () => createUser(role))
)
);
return users;
}
async function createQuestionTags(): Promise<QuestionTag[]> {
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<Question[]> {
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<T>(
check: () => Promise<T[]>,
create: () => Promise<T[]>,
entityName: string
): Promise<void> {
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<CodeTemplate[]> {
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<int> twoSum(vector<int>& nums, int target) {
}
};`,
correctSolution: `class Solution {
public:
vector<int> twoSum(vector<int> &nums, int target) {
unordered_map<int, int> 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<Integer, Integer> 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<char> 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<Character> 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<Event[]> {
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<EventQuestion[]> {
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();

5
src/lib/prisma.ts Normal file
View File

@ -0,0 +1,5 @@
import { PrismaClient } from "@prisma/client";
const prisma: PrismaClient = new PrismaClient();
export default prisma;