diff --git a/.env.example b/.env.example index 932b9f1e..d3c91282 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ PORT=4000 -DATABASE_URL="?schema=prisma" -SHADOW_DATABASE_URL="?schema=shadow" +DATABASE_URL= +SHADOW_DATABASE_URL= JWT_SECRET="somesecurestring" JWT_EXPIRY="24h" \ No newline at end of file diff --git a/prisma/migrations/20250227123420_better_posts/migration.sql b/prisma/migrations/20250227123420_better_posts/migration.sql new file mode 100644 index 00000000..ba5f5aa8 --- /dev/null +++ b/prisma/migrations/20250227123420_better_posts/migration.sql @@ -0,0 +1,45 @@ +/* + Warnings: + + - Added the required column `createdAt` to the `Post` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `Post` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Post" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- CreateTable +CREATE TABLE "Like" ( + "id" SERIAL NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Like_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" SERIAL NOT NULL, + "postId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 72ec5632..25758f38 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,6 +26,8 @@ model User { cohort Cohort? @relation(fields: [cohortId], references: [id]) posts Post[] deliveryLogs DeliveryLog[] + comments Comment[] + likes Like[] } model Profile { @@ -45,10 +47,35 @@ model Cohort { } model Post { - id Int @id @default(autoincrement()) - content String - userId Int - user User @relation(fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + content String + userId Int + user User @relation(fields: [userId], references: [id]) + likes Like[] + comments Comment[] + createdAt DateTime + updatedAt DateTime +} + +model Like { + id Int @id @default(autoincrement()) + postId Int + post Post @relation(fields: [postId], references: [id]) + userId Int + user User @relation(fields: [userId], references: [id]) + createdAt DateTime + updatedAt DateTime +} + +model Comment { + id Int @id @default(autoincrement()) + postId Int + post Post @relation(fields: [postId], references: [id]) + userId Int + user User @relation(fields: [userId], references: [id]) + content String + createdAt DateTime + updatedAt DateTime } model DeliveryLog { diff --git a/prisma/seed.js b/prisma/seed.js index 21684795..386f04fe 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -34,8 +34,12 @@ async function seed() { async function createPost(userId, content) { const post = await prisma.post.create({ data: { - userId, - content + userId: userId, + content: content, + // likes: [], + // comments: [], + createdAt: new Date(), + updatedAt: new Date() }, include: { user: true @@ -81,6 +85,8 @@ async function createUser( githubUrl } } + // comments: [], + // likes: [] }, include: { profile: true diff --git a/src/controllers/like.js b/src/controllers/like.js new file mode 100644 index 00000000..320a2eb2 --- /dev/null +++ b/src/controllers/like.js @@ -0,0 +1,14 @@ +import { sendDataResponse } from '../utils/responses' + +export const create = async (req, res) => { + const { postId, userId } = req.body + + try { + const post = await Post.findById + if (!(postId && userId)) + return sendDataResponse(res, 400, { content: 'Must provide post ID' }) + + } catch (error) {} + } + +} \ No newline at end of file diff --git a/src/controllers/post.js b/src/controllers/post.js index 7b168039..31dacf84 100644 --- a/src/controllers/post.js +++ b/src/controllers/post.js @@ -1,28 +1,41 @@ +import Post from '../domain/post.js' +import User from '../domain/user.js' import { sendDataResponse } from '../utils/responses.js' export const create = async (req, res) => { - const { content } = req.body + const timestamp = new Date() + const postToCreate = await Post.fromJson({ + ...req.body, + createdAt: timestamp, + updatedAt: timestamp + }) + + if (!User.findById(postToCreate.userId)) + return sendDataResponse(res, 400, { content: 'No user with ID' }) + + // if (!postToCreate.content) { + // return sendDataResponse(res, 400, { content: 'Must provide content' }) + // } - if (!content) { - return sendDataResponse(res, 400, { content: 'Must provide content' }) - } + const createdPost = await postToCreate.save() - return sendDataResponse(res, 201, { post: { id: 1, content } }) + return sendDataResponse(res, 201, { post: createdPost }) +} + +export const getAllByUserId = async (req, res) => { + const foundPosts = (await Post.findByUserId(req.body)).map((post) => { + return { ...post.toJson().post } + }) + return sendDataResponse(res, 200, { + posts: foundPosts + }) } export const getAll = async (req, res) => { + const foundPosts = (await Post.findAll()).map((post) => { + return { ...post.toJson().post } + }) return sendDataResponse(res, 200, { - posts: [ - { - id: 1, - content: 'Hello world!', - author: { ...req.user } - }, - { - id: 2, - content: 'Hello from the void!', - author: { ...req.user } - } - ] + posts: foundPosts }) } diff --git a/src/controllers/profile.js b/src/controllers/profile.js new file mode 100644 index 00000000..cca9b0d8 --- /dev/null +++ b/src/controllers/profile.js @@ -0,0 +1,48 @@ +import Profile from '../domain/profile.js' +import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' + +export const create = async (req, res) => { + const profileToCreate = await Profile.fromJson(req.body) + + try { + const existingProfileWithUser = await Profile.findByUserId( + profileToCreate.userId + ) + + if (existingProfileWithUser) { + return sendDataResponse(res, 400, { email: 'You already have a profile' }) + } + + const createdProfile = await profileToCreate.save() + + return sendDataResponse(res, 201, createdProfile) + } catch (error) { + return sendMessageResponse(res, 500, 'Unable to create new profile') + } +} + +export const getById = async (req, res) => { + const id = parseInt(req.params.id) + + try { + const foundProfile = await Profile.findById(id) + + if (!foundProfile) { + return sendDataResponse(res, 404, { id: 'User not found' }) + } + + return sendDataResponse(res, 200, foundProfile) + } catch (e) { + return sendMessageResponse(res, 500, 'Unable to get user') + } +} + +export const updateById = async (req, res) => { + const { user_id: userId } = req.body + + if (!userId) { + return sendDataResponse(res, 400, { user_id: 'User ID is required' }) + } + + return sendDataResponse(res, 201, { user: { user_id: userId } }) +} diff --git a/src/controllers/user.js b/src/controllers/user.js index 40ff0f1c..74801379 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -3,14 +3,24 @@ import { sendDataResponse, sendMessageResponse } from '../utils/responses.js' export const create = async (req, res) => { const userToCreate = await User.fromJson(req.body) - try { const existingUser = await User.findByEmail(userToCreate.email) + // eslint-disable-next-line + const passwordRegex = /(?=.*[a-z])(?=.*[A-Z{1,}])(?=.*[0-9{1,}])(?=.*[!-\/:-@[-`{-~{1,}]).{8,}/ + const emailRegex = /[^@]{1,}[@]{1}[^@]{1,}/ if (existingUser) { return sendDataResponse(res, 400, { email: 'Email already in use' }) } + if (!passwordRegex.test(req.body.password)) { + return sendDataResponse(res, 400, { password: 'Invalid password' }) + } + + if (!emailRegex.test(req.body.email)) { + return sendDataResponse(res, 400, { email: 'Invalid email' }) + } + const createdUser = await userToCreate.save() return sendDataResponse(res, 201, createdUser) diff --git a/src/domain/comment.js b/src/domain/comment.js new file mode 100644 index 00000000..994d475b --- /dev/null +++ b/src/domain/comment.js @@ -0,0 +1 @@ +export default class Comment {} diff --git a/src/domain/like.js b/src/domain/like.js new file mode 100644 index 00000000..f0705b95 --- /dev/null +++ b/src/domain/like.js @@ -0,0 +1,101 @@ +import dbClient from '../utils/dbClient' + +export default class Like { + static fromDb(like) { + return new Like( + like.id, + like.postId, + like.userId, + like.createdAt, + like.updatedAt + ) + } + + static async fromJson(json) { + const { postId, userId, createdAt, updatedAt } = json + return new Like(null, postId, userId, createdAt, updatedAt) + } + + constructor(id, postId, userId, createdAt, updatedAt) { + this.id = id + this.postId = postId + this.userId = userId + this.createdAt = createdAt + this.updatedAt = updatedAt + } + + toJson() { + return { + like: { + id: this.id, + postId: this.postId, + userId: this.userId, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } + } + + async save() { + const data = { + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + if (this.postId) { + data.postId = { + connectOrCreate: { + id: this.postId + } + } + } + + if (this.userId) { + data.user = { + connectOrCreate: { + id: this.userId + } + } + } + + const createdLike = await dbClient.like.create({ + data, + include: { + post: true, + user: true + } + }) + return Like.fromDb(createdLike) + } + + static async findAll() { + return Like._findMany() + } + + static async findByPostId(postId) { + return Like._findMany('postId', postId) + } + + static async findByUserId(userId) { + return Like._findMany('userId', userId) + } + + static async _findMany(key, value) { + const query = { + include: { + post: true + } + } + + if (key && value) { + query.where = { + post: { + [key]: value + } + } + } + + const foundLikes = await dbClient.like.findMany(query) + return foundLikes.map((like) => Like.fromDb(like)) + } +} diff --git a/src/domain/post.js b/src/domain/post.js new file mode 100644 index 00000000..4dc2ae0a --- /dev/null +++ b/src/domain/post.js @@ -0,0 +1,76 @@ +import dbClient from '../utils/dbClient.js' + +export default class Post { + static fromDb(post) { + return new Post( + post.id, + post.userId, + post.content, + post.createdAt, + post.updatedAt + ) + } + + static async fromJson(json) { + const { userId, content, createdAt, updatedAt } = json + return new Post(null, userId, content, createdAt, updatedAt) + } + + constructor(id, userId, content, createdAt, updatedAt) { + this.id = id + this.userId = userId + this.content = content + this.createdAt = createdAt + this.updatedAt = updatedAt + } + + toJson() { + return { + post: { + id: this.id, + userId: this.userId, + content: this.content, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } + } + + async save() { + const data = { + content: this.content, + userId: this.userId, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + const createdPost = await dbClient.post.create({ + data, + include: { + user: true + } + }) + return Post.fromDb(createdPost) + } + + static async findAll() { + return Post._findMany() + } + + static async findByUserId(userId) { + return Post._findMany('userId', userId) + } + + static async _findMany(key, value) { + const query = {} + + if (key && value) { + query.where = { + [key]: value + } + } + + const foundPosts = await dbClient.post.findMany(query) + return foundPosts.map((p) => Post.fromDb(p)) + } +} diff --git a/src/domain/profile.js b/src/domain/profile.js new file mode 100644 index 00000000..bb8ac84a --- /dev/null +++ b/src/domain/profile.js @@ -0,0 +1,115 @@ +import dbClient from '../utils/dbClient.js' + +export default class Profile { + static fromDb(profile) { + return new Profile( + profile.id, + profile.userId, + profile.firstName, + profile.lastName, + profile.bio, + profile.githubUrl + ) + } + + static async fromJson(json) { + const { userId, firstName, lastName, bio, githubUrl } = json + + return new Profile(null, userId, firstName, lastName, bio, githubUrl) + } + + constructor(id, userId, firstName, lastName, bio, githubUrl) { + this.id = id + this.userId = userId + this.firstName = firstName + this.lastName = lastName + this.bio = bio + this.githubUrl = githubUrl + } + + toJson() { + return { + profile: { + id: this.id, + user_id: this.userId, + firstName: this.firstName, + lastName: this.lastName, + bio: this.bio, + githubUrl: this.githubUrl + } + } + } + + async save() { + const data = { + firstName: this.firstName, + lastName: this.lastName, + bio: this.bio, + githubUrl: this.githubUrl + } + + if (this.userId) { + data.user = { + connectOrCreate: { + id: this.userId + } + } + } + + const createdProfile = await dbClient.profile.create({ + data, + include: { + user: true + } + }) + return Profile.fromDb(createdProfile) + } + + static async findById(id) { + return Profile._findByUnique('id', id) + } + + static async findAll() { + return Profile._findMany() + } + + static async findByUserId(userId) { + return Profile._findByUnique('userId', userId) + } + + static async _findByUnique(key, value) { + const foundProfile = await dbClient.profile.findUnique({ + where: { + [key]: value + }, + include: { + user: true + } + }) + + if (foundProfile) { + return Profile.fromDb(foundProfile) + } + + return null + } + + static async _findMany(key, value) { + const query = { + include: { + user: true + } + } + + if (key && value) { + query.where = { + user: { + [key]: value + } + } + } + + const foundProfiles = await dbClient.profile.findMany(query) + return foundProfiles.map((p) => Profile.fromDb(p)) + } +} diff --git a/src/routes/profile.js b/src/routes/profile.js new file mode 100644 index 00000000..3c06f06d --- /dev/null +++ b/src/routes/profile.js @@ -0,0 +1,12 @@ +import { Router } from 'express' +import { create, getById, updateById } from '../controllers/profile.js' +import { validateAuthentication } from '../middleware/auth.js' + +const router = Router() + +// Need to validate +router.post('/', validateAuthentication, create) +router.get('/:id', validateAuthentication, getById) +router.patch('/:id', validateAuthentication, updateById) + +export default router diff --git a/src/server.js b/src/server.js index a3f67eeb..4f99d33e 100644 --- a/src/server.js +++ b/src/server.js @@ -7,6 +7,7 @@ import cors from 'cors' import userRouter from './routes/user.js' import postRouter from './routes/post.js' import authRouter from './routes/auth.js' +import profileRouter from './routes/profile.js' import cohortRouter from './routes/cohort.js' import deliveryLogRouter from './routes/deliveryLog.js' @@ -21,6 +22,7 @@ const docFile = fs.readFileSync('./docs/openapi.yml', 'utf8') const swaggerDoc = YAML.parse(docFile) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc)) +app.use('/profile', profileRouter) app.use('/users', userRouter) app.use('/posts', postRouter) app.use('/cohorts', cohortRouter)