Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
"postinstall": "prisma generate",
"start": "node ./dist/server.js",
"test": "dotenv -e .env.test -o -- vitest",
"db:reset": "prisma migrate reset --skip-seed",
"dev": "tsx watch --env-file=.env ./src/server.ts",
"db:seed": "tsx --env-file=.env ./prisma/seed/run.ts",
"build": "tsc --pretty -p tsconfig.prod.json && tsc-alias -p tsconfig.prod.json",
"test:db:push": "dotenv -e .env.test -o -- prisma db push --skip-generate --force-reset",
"pg:down": "docker compose -f docker-compose.postgres.yml down --remove-orphans",
Expand Down Expand Up @@ -89,8 +91,5 @@
},
"simple-git-hooks": {
"pre-commit": "npm run lint && npm run type-check"
},
"prisma": {
"seed": "tsx ./prisma/seed/seed.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- Added the required column `updated_at` to the `posts_votes` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "public"."posts_votes" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL;
58 changes: 30 additions & 28 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ datasource db {
}

model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
order Int @unique @default(autoincrement())
password String @db.Char(60)
fullname String @db.VarChar(100)
username String @unique @db.VarChar(50)
isAdmin Boolean @default(false) @map("is_admin")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
votesOnPosts VoteOnPost[]
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
order Int @unique @default(autoincrement())
password String @db.Char(60)
fullname String @db.VarChar(100)
username String @unique @db.VarChar(50)
isAdmin Boolean @default(false) @map("is_admin")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
votesOnPosts VotesOnPosts[]
comments Comment[]
images Image[]
posts Post[]
Expand All @@ -41,20 +41,20 @@ model Comment {
}

model Post {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
order Int @unique @default(autoincrement())
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String @map("author_id") @db.Uuid
published Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
order Int @unique @default(autoincrement())
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId String @map("author_id") @db.Uuid
published Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
votes VotesOnPosts[]
tags TagsOnPosts[]
votes VoteOnPost[]
comments Comment[]
content String
title String
image Image? @relation(fields: [imageId], references: [id])
imageId String? @map("image_id") @db.Uuid
image Image? @relation(fields: [imageId], references: [id])
imageId String? @map("image_id") @db.Uuid

@@map("posts")
}
Expand All @@ -78,14 +78,16 @@ model TagsOnPosts {
@@map("posts_tags")
}

model VoteOnPost {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
order Int @unique @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @map("user_id") @db.Uuid
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String @map("post_id") @db.Uuid
isUpvote Boolean @default(true) @map("is_upvote")
model VotesOnPosts {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
order Int @unique @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @map("user_id") @db.Uuid
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String @map("post_id") @db.Uuid
isUpvote Boolean @default(true) @map("is_upvote")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([userId, postId])
@@map("posts_votes")
Expand All @@ -110,7 +112,7 @@ model Image {
xPos Int @default(0) @map("x_pos")
yPos Int @default(0) @map("y_pos")
scale Float @default(1.0)
Post Post[]
posts Post[]

@@map("images")
}
2 changes: 2 additions & 0 deletions prisma/seed/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './run';
export * from './run';
180 changes: 180 additions & 0 deletions prisma/seed/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { PrismaClient } from '../client';
import { faker } from '@faker-js/faker';
import bcrypt from 'bcryptjs';

const AUTHOR_PASSWORD = process.env.AUTHOR_PASSWORD;

if (!AUTHOR_PASSWORD) {
throw Error('The environment variable `AUTHOR_PASSWORD` is missed');
}

const titles = [
'Why TypeScript Is Worth the Learning Curve',
'Docker Compose for Local Development',
'Understanding JWT: Auth Made Simple',
'Top 5 VS Code Extensions for JavaScript Developers',
"REST vs. GraphQL: A Developer's Perspective",
'Mastering Zod for Schema Validation',
'How to Secure Your Express App',
'Deploying Node.js Apps with Koyeb',
'CSS-in-JS: Should You Use It in 2025?',
'How I Built a Portfolio While Learning to Code',
];

const postCount = titles.length;

const tags = [
'open_source',
'full_stack',
'javaScript',
'typeScript',
'security',
'frontend',
'software',
'testing',
'backend',
];

const db = new PrismaClient();

async function main() {
console.log('Resetting the database...');
await db.$transaction([
db.comment.deleteMany({}),
db.votesOnPosts.deleteMany({}),
db.tagsOnPosts.deleteMany({}),
db.post.deleteMany({}),
db.image.deleteMany({}),
db.user.deleteMany({}),
db.tag.deleteMany({}),
]);

console.log('Seeding the database with authors...');
const passHashArgs = [AUTHOR_PASSWORD, 10] as [string, number];
const dbPostAuthors = await db.user.createManyAndReturn({
data: [
{
password: bcrypt.hashSync(...passHashArgs),
bio: 'From Nowhere land with love.',
username: 'nowhere-man',
fullname: 'Nowhere-Man',
isAdmin: true,
},
{
password: bcrypt.hashSync(...passHashArgs),
fullname: 'Clark Kent / Kal-El',
bio: 'From Krypton with love.',
username: 'superman',
isAdmin: false,
},
{
password: bcrypt.hashSync(...passHashArgs),
bio: 'From Gotham with love.',
fullname: 'Bruce Wayne',
username: 'batman',
isAdmin: false,
},
],
});

console.log(
'Seeding the database with images, posts, categories, comment, and votes...'
);
for (let i = 0; i < postCount; i++) {
const postAuthor = faker.helpers.arrayElement(dbPostAuthors);
const dbPostViewers = dbPostAuthors.filter(
({ id }) => id !== postAuthor.id
);

const tagsCount = faker.number.int({ min: 2, max: 3 });
const postTags = faker.helpers.arrayElements(tags, tagsCount);

const gapDays = 42;
const dateFactor = postCount - i;
const dayMS = 24 * 60 * 60 * 1000;
const startDate = new Date(Date.now() - dateFactor * dayMS * gapDays);
const endDate = new Date(Date.now() - (dateFactor - 1) * dayMS * gapDays);
const postDate = faker.date.between({ from: startDate, to: endDate });

const dbImage = await db.image.create({
data: {
src: `https://ndauvqaezozccgtddhkr.supabase.co/storage/v1/object/public/images/seed/${i}.jpg`,
storageFullPath: `images/seed/${i}.jpg`,
storageId: crypto.randomUUID(),
ownerId: postAuthor.id,
mimetype: 'image/jpeg',
size: 7654321,
height: 1080,
width: 1920,
posts: {
create: [
{
published: true,
title: titles[i],
createdAt: postDate,
updatedAt: postDate,
authorId: postAuthor.id,
content: faker.lorem.paragraphs(
faker.number.int({ min: 10, max: 15 })
),
tags: {
create: postTags.map((name) => ({
tag: {
connectOrCreate: { where: { name }, create: { name } },
},
})),
},
},
],
},
},
include: { posts: true },
});
const dbPost = dbImage.posts[0];

const commentsCount = faker.number.int({ min: 3, max: 7 });
const commentDates = faker.date.betweens({
count: commentsCount,
from: postDate,
to: endDate,
});
await db.comment.createMany({
data: Array.from({ length: commentsCount }).map((_, commentIndex) => ({
content: faker.lorem.sentences(faker.number.int({ min: 1, max: 3 })),
authorId: faker.helpers.arrayElement(dbPostViewers).id,
createdAt: commentDates[commentIndex],
updatedAt: commentDates[commentIndex],
postId: dbPost.id,
})),
});

const votesDates = faker.date.betweens({
count: dbPostViewers.length,
from: postDate,
to: endDate,
});
await db.votesOnPosts.createMany({
data: dbPostViewers.map(({ id }, voteIndex) => ({
isUpvote: faker.helpers.arrayElement([true, false]),
createdAt: votesDates[voteIndex],
updatedAt: votesDates[voteIndex],
postId: dbPost.id,
userId: id,
})),
});
}
}

main()
.then(async () => {
await db.$disconnect();
})
.catch(async (e) => {
console.error(e);
await db.$disconnect();
process.exit(1);
});

export const seed = main;

export default main;
Loading