Skip to content

Commit 17fd352

Browse files
Merge PR: Refactor config, models, and DB seed
2 parents 1c0fab2 + 53a0763 commit 17fd352

11 files changed

Lines changed: 243 additions & 215 deletions

File tree

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
"postinstall": "prisma generate",
3838
"start": "node ./dist/server.js",
3939
"test": "dotenv -e .env.test -o -- vitest",
40+
"db:reset": "prisma migrate reset --skip-seed",
4041
"dev": "tsx watch --env-file=.env ./src/server.ts",
42+
"db:seed": "tsx --env-file=.env ./prisma/seed/run.ts",
4143
"build": "tsc --pretty -p tsconfig.prod.json && tsc-alias -p tsconfig.prod.json",
4244
"test:db:push": "dotenv -e .env.test -o -- prisma db push --skip-generate --force-reset",
4345
"pg:down": "docker compose -f docker-compose.postgres.yml down --remove-orphans",
@@ -89,8 +91,5 @@
8991
},
9092
"simple-git-hooks": {
9193
"pre-commit": "npm run lint && npm run type-check"
92-
},
93-
"prisma": {
94-
"seed": "tsx ./prisma/seed/seed.ts"
9594
}
9695
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
Warnings:
3+
4+
- 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.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "public"."posts_votes" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
9+
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL;

prisma/schema.prisma

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ datasource db {
99
}
1010

1111
model User {
12-
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
13-
order Int @unique @default(autoincrement())
14-
password String @db.Char(60)
15-
fullname String @db.VarChar(100)
16-
username String @unique @db.VarChar(50)
17-
isAdmin Boolean @default(false) @map("is_admin")
18-
createdAt DateTime @default(now()) @map("created_at")
19-
updatedAt DateTime @updatedAt @map("updated_at")
20-
votesOnPosts VoteOnPost[]
12+
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
13+
order Int @unique @default(autoincrement())
14+
password String @db.Char(60)
15+
fullname String @db.VarChar(100)
16+
username String @unique @db.VarChar(50)
17+
isAdmin Boolean @default(false) @map("is_admin")
18+
createdAt DateTime @default(now()) @map("created_at")
19+
updatedAt DateTime @updatedAt @map("updated_at")
20+
votesOnPosts VotesOnPosts[]
2121
comments Comment[]
2222
images Image[]
2323
posts Post[]
@@ -41,20 +41,20 @@ model Comment {
4141
}
4242

4343
model Post {
44-
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
45-
order Int @unique @default(autoincrement())
46-
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
47-
authorId String @map("author_id") @db.Uuid
48-
published Boolean @default(false)
49-
createdAt DateTime @default(now()) @map("created_at")
50-
updatedAt DateTime @updatedAt @map("updated_at")
44+
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
45+
order Int @unique @default(autoincrement())
46+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
47+
authorId String @map("author_id") @db.Uuid
48+
published Boolean @default(false)
49+
createdAt DateTime @default(now()) @map("created_at")
50+
updatedAt DateTime @updatedAt @map("updated_at")
51+
votes VotesOnPosts[]
5152
tags TagsOnPosts[]
52-
votes VoteOnPost[]
5353
comments Comment[]
5454
content String
5555
title String
56-
image Image? @relation(fields: [imageId], references: [id])
57-
imageId String? @map("image_id") @db.Uuid
56+
image Image? @relation(fields: [imageId], references: [id])
57+
imageId String? @map("image_id") @db.Uuid
5858
5959
@@map("posts")
6060
}
@@ -78,14 +78,16 @@ model TagsOnPosts {
7878
@@map("posts_tags")
7979
}
8080

81-
model VoteOnPost {
82-
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
83-
order Int @unique @default(autoincrement())
84-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
85-
userId String @map("user_id") @db.Uuid
86-
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
87-
postId String @map("post_id") @db.Uuid
88-
isUpvote Boolean @default(true) @map("is_upvote")
81+
model VotesOnPosts {
82+
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
83+
order Int @unique @default(autoincrement())
84+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
85+
userId String @map("user_id") @db.Uuid
86+
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
87+
postId String @map("post_id") @db.Uuid
88+
isUpvote Boolean @default(true) @map("is_upvote")
89+
createdAt DateTime @default(now()) @map("created_at")
90+
updatedAt DateTime @updatedAt @map("updated_at")
8991
9092
@@unique([userId, postId])
9193
@@map("posts_votes")
@@ -110,7 +112,7 @@ model Image {
110112
xPos Int @default(0) @map("x_pos")
111113
yPos Int @default(0) @map("y_pos")
112114
scale Float @default(1.0)
113-
Post Post[]
115+
posts Post[]
114116
115117
@@map("images")
116118
}

prisma/seed/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './run';
2+
export * from './run';

prisma/seed/run.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { PrismaClient } from '../client';
2+
import { faker } from '@faker-js/faker';
3+
import bcrypt from 'bcryptjs';
4+
5+
const AUTHOR_PASSWORD = process.env.AUTHOR_PASSWORD;
6+
7+
if (!AUTHOR_PASSWORD) {
8+
throw Error('The environment variable `AUTHOR_PASSWORD` is missed');
9+
}
10+
11+
const titles = [
12+
'Why TypeScript Is Worth the Learning Curve',
13+
'Docker Compose for Local Development',
14+
'Understanding JWT: Auth Made Simple',
15+
'Top 5 VS Code Extensions for JavaScript Developers',
16+
"REST vs. GraphQL: A Developer's Perspective",
17+
'Mastering Zod for Schema Validation',
18+
'How to Secure Your Express App',
19+
'Deploying Node.js Apps with Koyeb',
20+
'CSS-in-JS: Should You Use It in 2025?',
21+
'How I Built a Portfolio While Learning to Code',
22+
];
23+
24+
const postCount = titles.length;
25+
26+
const tags = [
27+
'open_source',
28+
'full_stack',
29+
'javaScript',
30+
'typeScript',
31+
'security',
32+
'frontend',
33+
'software',
34+
'testing',
35+
'backend',
36+
];
37+
38+
const db = new PrismaClient();
39+
40+
async function main() {
41+
console.log('Resetting the database...');
42+
await db.$transaction([
43+
db.comment.deleteMany({}),
44+
db.votesOnPosts.deleteMany({}),
45+
db.tagsOnPosts.deleteMany({}),
46+
db.post.deleteMany({}),
47+
db.image.deleteMany({}),
48+
db.user.deleteMany({}),
49+
db.tag.deleteMany({}),
50+
]);
51+
52+
console.log('Seeding the database with authors...');
53+
const passHashArgs = [AUTHOR_PASSWORD, 10] as [string, number];
54+
const dbPostAuthors = await db.user.createManyAndReturn({
55+
data: [
56+
{
57+
password: bcrypt.hashSync(...passHashArgs),
58+
bio: 'From Nowhere land with love.',
59+
username: 'nowhere-man',
60+
fullname: 'Nowhere-Man',
61+
isAdmin: true,
62+
},
63+
{
64+
password: bcrypt.hashSync(...passHashArgs),
65+
fullname: 'Clark Kent / Kal-El',
66+
bio: 'From Krypton with love.',
67+
username: 'superman',
68+
isAdmin: false,
69+
},
70+
{
71+
password: bcrypt.hashSync(...passHashArgs),
72+
bio: 'From Gotham with love.',
73+
fullname: 'Bruce Wayne',
74+
username: 'batman',
75+
isAdmin: false,
76+
},
77+
],
78+
});
79+
80+
console.log(
81+
'Seeding the database with images, posts, categories, comment, and votes...'
82+
);
83+
for (let i = 0; i < postCount; i++) {
84+
const postAuthor = faker.helpers.arrayElement(dbPostAuthors);
85+
const dbPostViewers = dbPostAuthors.filter(
86+
({ id }) => id !== postAuthor.id
87+
);
88+
89+
const tagsCount = faker.number.int({ min: 2, max: 3 });
90+
const postTags = faker.helpers.arrayElements(tags, tagsCount);
91+
92+
const gapDays = 42;
93+
const dateFactor = postCount - i;
94+
const dayMS = 24 * 60 * 60 * 1000;
95+
const startDate = new Date(Date.now() - dateFactor * dayMS * gapDays);
96+
const endDate = new Date(Date.now() - (dateFactor - 1) * dayMS * gapDays);
97+
const postDate = faker.date.between({ from: startDate, to: endDate });
98+
99+
const dbImage = await db.image.create({
100+
data: {
101+
src: `https://ndauvqaezozccgtddhkr.supabase.co/storage/v1/object/public/images/seed/${i}.jpg`,
102+
storageFullPath: `images/seed/${i}.jpg`,
103+
storageId: crypto.randomUUID(),
104+
ownerId: postAuthor.id,
105+
mimetype: 'image/jpeg',
106+
size: 7654321,
107+
height: 1080,
108+
width: 1920,
109+
posts: {
110+
create: [
111+
{
112+
published: true,
113+
title: titles[i],
114+
createdAt: postDate,
115+
updatedAt: postDate,
116+
authorId: postAuthor.id,
117+
content: faker.lorem.paragraphs(
118+
faker.number.int({ min: 10, max: 15 })
119+
),
120+
tags: {
121+
create: postTags.map((name) => ({
122+
tag: {
123+
connectOrCreate: { where: { name }, create: { name } },
124+
},
125+
})),
126+
},
127+
},
128+
],
129+
},
130+
},
131+
include: { posts: true },
132+
});
133+
const dbPost = dbImage.posts[0];
134+
135+
const commentsCount = faker.number.int({ min: 3, max: 7 });
136+
const commentDates = faker.date.betweens({
137+
count: commentsCount,
138+
from: postDate,
139+
to: endDate,
140+
});
141+
await db.comment.createMany({
142+
data: Array.from({ length: commentsCount }).map((_, commentIndex) => ({
143+
content: faker.lorem.sentences(faker.number.int({ min: 1, max: 3 })),
144+
authorId: faker.helpers.arrayElement(dbPostViewers).id,
145+
createdAt: commentDates[commentIndex],
146+
updatedAt: commentDates[commentIndex],
147+
postId: dbPost.id,
148+
})),
149+
});
150+
151+
const votesDates = faker.date.betweens({
152+
count: dbPostViewers.length,
153+
from: postDate,
154+
to: endDate,
155+
});
156+
await db.votesOnPosts.createMany({
157+
data: dbPostViewers.map(({ id }, voteIndex) => ({
158+
isUpvote: faker.helpers.arrayElement([true, false]),
159+
createdAt: votesDates[voteIndex],
160+
updatedAt: votesDates[voteIndex],
161+
postId: dbPost.id,
162+
userId: id,
163+
})),
164+
});
165+
}
166+
}
167+
168+
main()
169+
.then(async () => {
170+
await db.$disconnect();
171+
})
172+
.catch(async (e) => {
173+
console.error(e);
174+
await db.$disconnect();
175+
process.exit(1);
176+
});
177+
178+
export const seed = main;
179+
180+
export default main;

0 commit comments

Comments
 (0)