├── Procfile ├── src ├── constants.js ├── pubsub.js ├── client.js ├── users │ ├── me │ │ ├── me.typeDefs.js │ │ └── me.resolvers.js │ ├── seeProfile │ │ ├── seeProfile.typeDefs.js │ │ └── seeProfile.resolvers.js │ ├── searchUsers │ │ ├── searchUsers.typeDefs.js │ │ └── searchUsers.resolvers.js │ ├── followUser │ │ ├── followUser.typeDefs.js │ │ └── followUser.resolvers.js │ ├── unfollowUser │ │ ├── unfollowUser.typeDefs.js │ │ └── unfollowUser.resolvers.js │ ├── login │ │ ├── login.typeDefs.js │ │ └── login.resolvers.js │ ├── seeFollowing │ │ ├── seeFollowing.typeDefs.js │ │ └── seeFollowing.resolvers.js │ ├── createAccount │ │ ├── createAccount.typeDefs.js │ │ └── createAccount.resolvers.js │ ├── seeFollowers │ │ ├── seeFollowers.typeDefs.js │ │ └── seeFollowers.resolvers.js │ ├── editProfile │ │ ├── editProfile.typeDefs.js │ │ └── editProfile.resolvers.js │ ├── users.typeDefs.js │ ├── users.utils.js │ └── users.resolvers.js ├── messages │ ├── seeRooms │ │ ├── seeRooms.typeDefs.js │ │ └── seeRooms.resolvers.js │ ├── seeRoom │ │ ├── seeRoom.typeDefs.js │ │ └── seeRoom.resolvers.js │ ├── roomUpdates │ │ ├── roomUpdates.typeDefs.js │ │ └── roomUpdates.resolvers.js │ ├── readMessage │ │ ├── readMessage.typeDefs.js │ │ └── readMessage.resolvers.js │ ├── sendMessage │ │ ├── sendMessage.typeDefs.js │ │ └── sendMessage.resolvers.js │ ├── messages.typeDefs.js │ └── messages.resolvers.js ├── photos │ ├── seePhoto │ │ ├── seePhoto.typeDefs.js │ │ └── seePhoto.resolvers.js │ ├── seeFeed │ │ ├── seeFeed.typeDefs.js │ │ └── seeFeed.resolvers.js │ ├── seeHashtag │ │ ├── seeHashtag.typeDefs.js │ │ └── seeHashtag.resolvers.js │ ├── seePhotoLikes │ │ ├── seePhotoLikes.typeDefs.js │ │ └── seePhotoLikes.resolvers.js │ ├── deletePhoto │ │ ├── deletePhoto.typeDefs.js │ │ └── deletePhoto.resolvers.js │ ├── searchPhotos │ │ ├── searchPhotos.typeDefs.js │ │ └── searchPhotos.resolvers.js │ ├── toggleLike │ │ ├── toggleLike.typeDefs.js │ │ └── toggleLike.resolvers.js │ ├── seePhotoComments │ │ ├── seePhotoComments.typeDefs.js │ │ └── seePhotoComments.resolvers.js │ ├── uploadPhoto │ │ ├── uploadPhoto.typeDefs.js │ │ └── uploadPhoto.resolvers.js │ ├── editPhoto │ │ ├── editPhoto.typeDefs.js │ │ └── editPhoto.resolvers.js │ ├── photos.utils.js │ ├── photos.typeDefs.js │ └── photos.resolvers.js ├── comments │ ├── deleteComment │ │ ├── deleteComment.typeDefs.js │ │ └── deleteComment.resolvers.js │ ├── editComment │ │ ├── editComment.typeDefs.js │ │ └── editComment.resolvers.js │ ├── createComment │ │ ├── createComment.typeDefs.js │ │ └── createComment.resolvers.js │ ├── comments.resolvers.js │ └── comments.typeDefs.js ├── shared │ ├── shared.typeDefs.js │ └── shared.utils.js ├── schema.js └── server.js ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20210210080039_default_seen │ │ └── migration.sql │ ├── 20210210080607_read_default │ │ └── migration.sql │ ├── 20210126054209_bio_avatart │ │ └── migration.sql │ ├── 20210210075734_message_seen │ │ └── migration.sql │ ├── 20210204071024_unique_hashtag │ │ └── migration.sql │ ├── 20210205103644_unique_together │ │ └── migration.sql │ ├── 20210114091336_created_at_updated_at │ │ └── migration.sql │ ├── 20210210080333_rad │ │ └── migration.sql │ ├── 20210114090955_user_model │ │ └── migration.sql │ ├── 20210205103236_likes │ │ └── migration.sql │ ├── 20210127094541_follows │ │ └── migration.sql │ ├── 20210208061544_comments │ │ └── migration.sql │ ├── 20210204061820_photos │ │ └── migration.sql │ └── 20210210064620_messages │ │ └── migration.sql └── schema.prisma ├── babel.config.json ├── README.MD ├── package.json └── .gitignore /Procfile: -------------------------------------------------------------------------------- 1 | release: npx prisma migrate deploy 2 | web: npm start -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const NEW_MESSAGE = "NEW_MESSAGE"; 2 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | provider = "postgresql" -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /src/pubsub.js: -------------------------------------------------------------------------------- 1 | import { PubSub } from "apollo-server-express"; 2 | 3 | const pubsub = new PubSub(); 4 | 5 | export default pubsub; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20210210080039_default_seen/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Message" ALTER COLUMN "seen" SET DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20210210080607_read_default/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Message" ALTER COLUMN "read" SET DEFAULT false; 3 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const client = new PrismaClient(); 4 | 5 | export default client; 6 | -------------------------------------------------------------------------------- /src/users/me/me.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | me: User 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /prisma/migrations/20210126054209_bio_avatart/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "bio" TEXT, 3 | ADD COLUMN "avatar" TEXT; 4 | -------------------------------------------------------------------------------- /src/messages/seeRooms/seeRooms.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seeRooms: [Room] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/messages/seeRoom/seeRoom.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seeRoom(id: Int!): Room 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/seePhoto/seePhoto.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seePhoto(id: Int!): Photo 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/seeFeed/seeFeed.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seeFeed(offset: Int!): [Photo] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/users/seeProfile/seeProfile.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seeProfile(username: String!): User 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/messages/roomUpdates/roomUpdates.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Subscription { 5 | roomUpdates(id: Int!): Message 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/seeHashtag/seeHashtag.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seeHashtag(hashtag: String!): Hashtag 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/seePhotoLikes/seePhotoLikes.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seePhotoLikes(id: Int!): [User] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/users/searchUsers/searchUsers.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | searchUsers(keyword: String!): [User] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/deletePhoto/deletePhoto.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | deletePhoto(id: Int!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/searchPhotos/searchPhotos.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | searchPhotos(keyword: String!): [Photo] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/toggleLike/toggleLike.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | toggleLike(id: Int!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/messages/readMessage/readMessage.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | readMessage(id: Int!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/seePhotoComments/seePhotoComments.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Query { 5 | seePhotoComments(id: Int!): [Comment] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/users/followUser/followUser.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | followUser(username: String!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/comments/deleteComment/deleteComment.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | deleteComment(id: Int!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/uploadPhoto/uploadPhoto.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | uploadPhoto(file: Upload!, caption: String): Photo 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/shared/shared.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type MutationResponse { 5 | ok: Boolean! 6 | id: Int 7 | error: String 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/users/unfollowUser/unfollowUser.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | unfollowUser(username: String!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/editPhoto/editPhoto.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | editPhoto(id: Int!, caption: String!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/comments/editComment/editComment.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | editComment(id: Int!, payload: String!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/comments/createComment/createComment.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | createComment(photoId: Int!, payload: String!): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/messages/sendMessage/sendMessage.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | sendMessage(payload: String!, roomId: Int, userId: Int): MutationResponse! 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/photos/photos.utils.js: -------------------------------------------------------------------------------- 1 | export const processHashtags = (caption) => { 2 | const hashtags = caption.match(/#[\w]+/g) || []; 3 | return hashtags.map((hashtag) => ({ 4 | where: { hashtag }, 5 | create: { hashtag }, 6 | })); 7 | }; 8 | -------------------------------------------------------------------------------- /src/comments/comments.resolvers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Comment: { 3 | isMine: ({ userId }, _, { loggedInUser }) => { 4 | if (!loggedInUser) { 5 | return false; 6 | } 7 | return userId === loggedInUser.id; 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/photos/seePhoto/seePhoto.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seePhoto: (_, { id }) => 6 | client.photo.findUnique({ 7 | where: { 8 | id, 9 | }, 10 | }), 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/photos/seeHashtag/seeHashtag.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seeHashtag: (_, { hashtag }) => 6 | client.hashtag.findUnique({ 7 | where: { 8 | hashtag, 9 | }, 10 | }), 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/comments/comments.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Comment { 5 | id: Int! 6 | user: User! 7 | photo: Photo! 8 | payload: String! 9 | isMine: Boolean! 10 | createdAt: String! 11 | updatedAt: String! 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /src/users/login/login.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type LoginResult { 5 | ok: Boolean! 6 | token: String 7 | error: String 8 | } 9 | type Mutation { 10 | login(username: String!, password: String!): LoginResult! 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20210210075734_message_seen/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `seen` to the `Message` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Message" ADD COLUMN "seen" BOOLEAN NOT NULL; 9 | -------------------------------------------------------------------------------- /src/users/seeFollowing/seeFollowing.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type SeeFollowingResult { 5 | ok: Boolean! 6 | error: String 7 | following: [User] 8 | } 9 | type Query { 10 | seeFollowing(username: String!, lastId: Int): SeeFollowingResult! 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/users/createAccount/createAccount.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | createAccount( 6 | firstName: String! 7 | lastName: String 8 | username: String! 9 | email: String! 10 | password: String! 11 | ): MutationResponse! 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20210204071024_unique_hashtag/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The migration will add a unique constraint covering the columns `[hashtag]` on the table `Hashtag`. If there are existing duplicate values, the migration will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Hashtag.hashtag_unique" ON "Hashtag"("hashtag"); 9 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | import { loadFilesSync, mergeResolvers, mergeTypeDefs } from "graphql-tools"; 2 | 3 | const loadedTypes = loadFilesSync(`${__dirname}/**/*.typeDefs.js`); 4 | const loadedResolvers = loadFilesSync(`${__dirname}/**/*.resolvers.js`); 5 | 6 | export const typeDefs = mergeTypeDefs(loadedTypes); 7 | export const resolvers = mergeResolvers(loadedResolvers); 8 | -------------------------------------------------------------------------------- /src/users/seeFollowers/seeFollowers.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type SeeFollowersResult { 5 | ok: Boolean! 6 | error: String 7 | followers: [User] 8 | totalPages: Int 9 | } 10 | type Query { 11 | seeFollowers(username: String!, page: Int!): SeeFollowersResult! 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20210205103644_unique_together/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The migration will add a unique constraint covering the columns `[photoId,userId]` on the table `Like`. If there are existing duplicate values, the migration will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Like.photoId_userId_unique" ON "Like"("photoId", "userId"); 9 | -------------------------------------------------------------------------------- /src/users/searchUsers/searchUsers.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | searchUsers: async (_, { keyword }) => 6 | client.user.findMany({ 7 | where: { 8 | username: { 9 | startsWith: keyword.toLowerCase(), 10 | }, 11 | }, 12 | }), 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/photos/searchPhotos/searchPhotos.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | searchPhotos: (_, { keyword }) => { 6 | return client.photo.findMany({ 7 | where: { 8 | caption: { 9 | contains: keyword, 10 | }, 11 | }, 12 | }); 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/users/me/me.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../users.utils"; 3 | 4 | export default { 5 | Query: { 6 | me: protectedResolver((_, __, { loggedInUser }) => 7 | client.user.findUnique({ 8 | where: { 9 | id: loggedInUser.id, 10 | }, 11 | }) 12 | ), 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/photos/seePhotoComments/seePhotoComments.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seePhotoComments: (_, { id }) => 6 | client.comment.findMany({ 7 | where: { 8 | photoId: id, 9 | }, 10 | orderBy: { 11 | createdAt: "asc", 12 | }, 13 | }), 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/users/editProfile/editProfile.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Mutation { 5 | editProfile( 6 | firstName: String 7 | lastName: String 8 | username: String 9 | email: String 10 | password: String 11 | bio: String 12 | avatar: Upload 13 | ): MutationResponse! 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/users/seeProfile/seeProfile.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seeProfile: (_, { username }) => 6 | client.user.findUnique({ 7 | where: { 8 | username, 9 | }, 10 | include: { 11 | following: true, 12 | followers: true, 13 | }, 14 | }), 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20210114091336_created_at_updated_at/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20210210080333_rad/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `seen` on the `Message` table. All the data in the column will be lost. 5 | - Added the required column `read` to the `Message` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Message" DROP COLUMN "seen", 10 | ADD COLUMN "read" BOOLEAN NOT NULL; 11 | -------------------------------------------------------------------------------- /src/photos/seePhotoLikes/seePhotoLikes.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seePhotoLikes: async (_, { id }) => { 6 | const likes = await client.like.findMany({ 7 | where: { 8 | photoId: id, 9 | }, 10 | select: { 11 | user: true, 12 | }, 13 | }); 14 | return likes.map((like) => like.user); 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/messages/messages.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Message { 5 | id: Int! 6 | payload: String! 7 | user: User! 8 | room: Room! 9 | read: Boolean! 10 | createdAt: String! 11 | updatedAt: String! 12 | } 13 | type Room { 14 | id: Int! 15 | unreadTotal: Int! 16 | users: [User] 17 | messages: [Message] 18 | createdAt: String! 19 | updatedAt: String! 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/messages/seeRooms/seeRooms.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Query: { 6 | seeRooms: protectedResolver(async (_, __, { loggedInUser }) => 7 | client.room.findMany({ 8 | where: { 9 | users: { 10 | some: { 11 | id: loggedInUser.id, 12 | }, 13 | }, 14 | }, 15 | }) 16 | ), 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /prisma/migrations/20210114090955_user_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL, 4 | "firstName" TEXT NOT NULL, 5 | "lastName" TEXT, 6 | "username" TEXT NOT NULL, 7 | "email" TEXT NOT NULL, 8 | "password" TEXT NOT NULL, 9 | 10 | PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "User.username_unique" ON "User"("username"); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); 18 | -------------------------------------------------------------------------------- /src/messages/seeRoom/seeRoom.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Query: { 6 | seeRoom: protectedResolver((_, { id }, { loggedInUser }) => 7 | client.room.findFirst({ 8 | where: { 9 | id, 10 | users: { 11 | some: { 12 | id: loggedInUser.id, 13 | }, 14 | }, 15 | }, 16 | }) 17 | ), 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/users/users.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type User { 5 | id: Int! 6 | firstName: String! 7 | lastName: String 8 | username: String! 9 | email: String! 10 | createdAt: String! 11 | updatedAt: String! 12 | bio: String 13 | avatar: String 14 | photos: [Photo] 15 | following: [User] 16 | followers: [User] 17 | totalFollowing: Int! 18 | totalFollowers: Int! 19 | isMe: Boolean! 20 | isFollowing: Boolean! 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20210205103236_likes/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Like" ( 3 | "id" SERIAL NOT NULL, 4 | "photoId" INTEGER NOT NULL, 5 | "userId" INTEGER NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL, 8 | 9 | PRIMARY KEY ("id") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Like" ADD FOREIGN KEY ("photoId") REFERENCES "Photo"("id") ON DELETE CASCADE ON UPDATE CASCADE; 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "Like" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20210127094541_follows/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "_FollowRelation" ( 3 | "A" INTEGER NOT NULL, 4 | "B" INTEGER NOT NULL 5 | ); 6 | 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "_FollowRelation_AB_unique" ON "_FollowRelation"("A", "B"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "_FollowRelation_B_index" ON "_FollowRelation"("B"); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "_FollowRelation" ADD FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "_FollowRelation" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20210208061544_comments/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Comment" ( 3 | "id" SERIAL NOT NULL, 4 | "payload" TEXT NOT NULL, 5 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "updatedAt" TIMESTAMP(3) NOT NULL, 7 | "userId" INTEGER NOT NULL, 8 | "photoId" INTEGER NOT NULL, 9 | 10 | PRIMARY KEY ("id") 11 | ); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Comment" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Comment" ADD FOREIGN KEY ("photoId") REFERENCES "Photo"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /src/shared/shared.utils.js: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | 3 | export const uploadToS3 = async (file, userId, folderName) => { 4 | AWS.config.update({ 5 | credentials: { 6 | accessKeyId: process.env.AWS_KEY, 7 | secretAccessKey: process.env.AWS_SECRET, 8 | }, 9 | }); 10 | const { filename, createReadStream } = await file; 11 | const readStream = createReadStream(); 12 | const objectName = `${folderName}/${userId}-${Date.now()}-${filename}`; 13 | const { Location } = await new AWS.S3() 14 | .upload({ 15 | Bucket: "instaclone-uploads", 16 | Key: objectName, 17 | ACL: "public-read", 18 | Body: readStream, 19 | }) 20 | .promise(); 21 | return Location; 22 | }; 23 | -------------------------------------------------------------------------------- /src/photos/photos.typeDefs.js: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | export default gql` 4 | type Photo { 5 | id: Int! 6 | user: User! 7 | file: String! 8 | caption: String 9 | likes: Int! 10 | commentNumber: Int! 11 | comments: [Comment] 12 | hashtags: [Hashtag] 13 | createdAt: String! 14 | updatedAt: String! 15 | isMine: Boolean! 16 | isLiked: Boolean! 17 | } 18 | type Hashtag { 19 | id: Int! 20 | hashtag: String! 21 | photos(page: Int!): [Photo] 22 | totalPhotos: Int! 23 | createdAt: String! 24 | updatedAt: String! 25 | } 26 | type Like { 27 | id: Int! 28 | photo: Photo! 29 | createdAt: String! 30 | updatedAt: String! 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /src/users/seeFollowing/seeFollowing.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seeFollowing: async (_, { username, lastId }) => { 6 | const ok = await client.user.findUnique({ 7 | where: { username }, 8 | select: { id: true }, 9 | }); 10 | if (!ok) { 11 | return { 12 | ok: false, 13 | error: "User not found", 14 | }; 15 | } 16 | const following = await client.user 17 | .findUnique({ where: { username } }) 18 | .following({ 19 | take: 5, 20 | skip: lastId ? 1 : 0, 21 | ...(lastId && { cursor: { id: lastId } }), 22 | }); 23 | return { 24 | ok: true, 25 | following, 26 | }; 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/photos/seeFeed/seeFeed.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Query: { 6 | seeFeed: protectedResolver((_, { offset }, { loggedInUser }) => 7 | client.photo.findMany({ 8 | take: 2, 9 | skip: offset, 10 | where: { 11 | OR: [ 12 | { 13 | user: { 14 | followers: { 15 | some: { 16 | id: loggedInUser.id, 17 | }, 18 | }, 19 | }, 20 | }, 21 | { 22 | userId: loggedInUser.id, 23 | }, 24 | ], 25 | }, 26 | orderBy: { 27 | createdAt: "desc", 28 | }, 29 | }) 30 | ), 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/users/followUser/followUser.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | followUser: protectedResolver(async (_, { username }, { loggedInUser }) => { 7 | const ok = await client.user.findUnique({ where: { username } }); 8 | if (!ok) { 9 | return { 10 | ok: false, 11 | error: "That user does not exist.", 12 | }; 13 | } 14 | await client.user.update({ 15 | where: { 16 | id: loggedInUser.id, 17 | }, 18 | data: { 19 | following: { 20 | connect: { 21 | username, 22 | }, 23 | }, 24 | }, 25 | }); 26 | return { 27 | ok: true, 28 | }; 29 | }), 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/users/login/login.resolvers.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import jwt from "jsonwebtoken"; 3 | import client from "../../client"; 4 | 5 | export default { 6 | Mutation: { 7 | login: async (_, { username, password }) => { 8 | const user = await client.user.findFirst({ where: { username } }); 9 | if (!user) { 10 | return { 11 | ok: false, 12 | error: "User not found.", 13 | }; 14 | } 15 | const passwordOk = await bcrypt.compare(password, user.password); 16 | if (!passwordOk) { 17 | return { 18 | ok: false, 19 | error: "Incorrect password.", 20 | }; 21 | } 22 | const token = await jwt.sign({ id: user.id }, process.env.SECRET_KEY); 23 | return { 24 | ok: true, 25 | token, 26 | }; 27 | }, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/messages/messages.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../client"; 2 | 3 | export default { 4 | Room: { 5 | users: ({ id }) => client.room.findUnique({ where: { id } }).users(), 6 | messages: ({ id }) => 7 | client.message.findMany({ 8 | where: { 9 | roomId: id, 10 | }, 11 | orderBy: { 12 | createdAt: "asc", 13 | }, 14 | }), 15 | unreadTotal: ({ id }, _, { loggedInUser }) => { 16 | if (!loggedInUser) { 17 | return 0; 18 | } 19 | return client.message.count({ 20 | where: { 21 | read: false, 22 | roomId: id, 23 | user: { 24 | id: { 25 | not: loggedInUser.id, 26 | }, 27 | }, 28 | }, 29 | }); 30 | }, 31 | }, 32 | Message: { 33 | user: ({ id }) => client.message.findUnique({ where: { id } }).user(), 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/users/seeFollowers/seeFollowers.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | 3 | export default { 4 | Query: { 5 | seeFollowers: async (_, { username, page }) => { 6 | const ok = await client.user.findUnique({ 7 | where: { username }, 8 | select: { id: true }, 9 | }); 10 | if (!ok) { 11 | return { 12 | ok: false, 13 | error: "User not found", 14 | }; 15 | } 16 | const followers = await client.user 17 | .findUnique({ where: { username } }) 18 | .followers({ 19 | take: 5, 20 | skip: (page - 1) * 5, 21 | }); 22 | const totalFollowers = await client.user.count({ 23 | where: { following: { some: { username } } }, 24 | }); 25 | return { 26 | ok: true, 27 | followers, 28 | totalPages: Math.ceil(totalFollowers / 5), 29 | }; 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/users/unfollowUser/unfollowUser.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | unfollowUser: protectedResolver( 7 | async (_, { username }, { loggedInUser }) => { 8 | const ok = await client.user.findUnique({ 9 | where: { username }, 10 | }); 11 | if (!ok) { 12 | return { 13 | ok: false, 14 | error: "Can't unfollow user.", 15 | }; 16 | } 17 | await client.user.update({ 18 | where: { 19 | id: loggedInUser.id, 20 | }, 21 | data: { 22 | following: { 23 | disconnect: { 24 | username, 25 | }, 26 | }, 27 | }, 28 | }); 29 | return { 30 | ok: true, 31 | }; 32 | } 33 | ), 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/photos/deletePhoto/deletePhoto.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | deletePhoto: protectedResolver(async (_, { id }, { loggedInUser }) => { 7 | const photo = await client.photo.findUnique({ 8 | where: { 9 | id, 10 | }, 11 | select: { 12 | userId: true, 13 | }, 14 | }); 15 | if (!photo) { 16 | return { 17 | ok: false, 18 | error: "Photo not found.", 19 | }; 20 | } else if (photo.userId !== loggedInUser.id) { 21 | return { 22 | ok: false, 23 | error: "Not authorized.", 24 | }; 25 | } else { 26 | await client.photo.delete({ 27 | where: { 28 | id, 29 | }, 30 | }); 31 | return { 32 | ok: true, 33 | }; 34 | } 35 | }), 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/users/users.utils.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import client from "../client"; 3 | 4 | export const getUser = async (token) => { 5 | try { 6 | if (!token) { 7 | return null; 8 | } 9 | const { id } = await jwt.verify(token, process.env.SECRET_KEY); 10 | const user = await client.user.findUnique({ where: { id } }); 11 | if (user) { 12 | return user; 13 | } else { 14 | return null; 15 | } 16 | } catch { 17 | return null; 18 | } 19 | }; 20 | 21 | export function protectedResolver(ourResolver) { 22 | return function (root, args, context, info) { 23 | if (!context.loggedInUser) { 24 | const query = info.operation.operation === "query"; 25 | if (query) { 26 | return null; 27 | } else { 28 | return { 29 | ok: false, 30 | error: "Please log in to perform this action.", 31 | }; 32 | } 33 | } 34 | return ourResolver(root, args, context, info); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/comments/deleteComment/deleteComment.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | deleteComment: protectedResolver(async (_, { id }, { loggedInUser }) => { 7 | const comment = await client.comment.findUnique({ 8 | where: { 9 | id, 10 | }, 11 | select: { 12 | userId: true, 13 | }, 14 | }); 15 | if (!comment) { 16 | return { 17 | ok: false, 18 | error: "Comment not found.", 19 | }; 20 | } else if (comment.userId !== loggedInUser.id) { 21 | return { 22 | ok: false, 23 | error: "Not authorized.", 24 | }; 25 | } else { 26 | await client.comment.delete({ 27 | where: { 28 | id, 29 | }, 30 | }); 31 | return { 32 | ok: true, 33 | }; 34 | } 35 | }), 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Instaclone 2 | 3 | Instaclone Backend. 4 | 5 | ## User: 6 | 7 | - [x] Create Account 8 | - [x] See Profile 9 | - [x] Login 10 | - [x] Edit Profile 11 | - [x] Change Avatar (Image Upload) 12 | - [x] Follow User 13 | - [x] Unfollow User 14 | - [x] See Followers w/ Pagination 15 | - [x] See Following w/ Pagination 16 | - [x] Computed Fields 17 | - [x] Search Users 18 | 19 | ## Photos 20 | 21 | - [x] Upload Photo (Parse #) 22 | - [x] See Photo 23 | - [x] See Hashtag 24 | - [x] Search Photos 25 | - [x] Edit Photo 26 | - [x] Like / Unlike Photo 27 | - [x] See Photo Likes 28 | - [x] See Feed 29 | - [x] See Photo Comments 30 | - [x] Delete Photo 31 | 32 | ## Comments 33 | 34 | - [x] Comment on Photo 35 | - [x] Delete Comment 36 | - [x] Edit Comment 37 | 38 | ## Refactor 39 | 40 | - [x] Mutation Responses 41 | 42 | ## Extras 43 | 44 | - [x] S3 Image Upload 45 | 46 | ## DMs 47 | 48 | - [x] See Rooms 49 | - [x] Send Message (Create Room) 50 | - [x] See Room 51 | - [x] Computed Fields 52 | - [x] See (Read) Message 53 | - [x] Realtime Messages 54 | -------------------------------------------------------------------------------- /src/photos/uploadPhoto/uploadPhoto.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { uploadToS3 } from "../../shared/shared.utils"; 3 | import { protectedResolver } from "../../users/users.utils"; 4 | import { processHashtags } from "../photos.utils"; 5 | 6 | export default { 7 | Mutation: { 8 | uploadPhoto: protectedResolver( 9 | async (_, { file, caption }, { loggedInUser }) => { 10 | let hashtagObj = []; 11 | if (caption) { 12 | hashtagObj = processHashtags(caption); 13 | } 14 | const fileUrl = await uploadToS3(file, loggedInUser.id, "uploads"); 15 | return client.photo.create({ 16 | data: { 17 | file: fileUrl, 18 | caption, 19 | user: { 20 | connect: { 21 | id: loggedInUser.id, 22 | }, 23 | }, 24 | ...(hashtagObj.length > 0 && { 25 | hashtags: { 26 | connectOrCreate: hashtagObj, 27 | }, 28 | }), 29 | }, 30 | }); 31 | } 32 | ), 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/messages/readMessage/readMessage.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | readMessage: protectedResolver(async (_, { id }, { loggedInUser }) => { 7 | const message = await client.message.findFirst({ 8 | where: { 9 | id, 10 | userId: { 11 | not: loggedInUser.id, 12 | }, 13 | room: { 14 | users: { 15 | some: { 16 | id: loggedInUser.id, 17 | }, 18 | }, 19 | }, 20 | }, 21 | select: { 22 | id: true, 23 | }, 24 | }); 25 | if (!message) { 26 | return { 27 | ok: false, 28 | error: "Message not found.", 29 | }; 30 | } 31 | await client.message.update({ 32 | where: { 33 | id, 34 | }, 35 | data: { 36 | read: true, 37 | }, 38 | }); 39 | return { 40 | ok: true, 41 | }; 42 | }), 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/comments/editComment/editComment.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | editComment: protectedResolver( 7 | async (_, { id, payload }, { loggedInUser }) => { 8 | const comment = await client.comment.findUnique({ 9 | where: { 10 | id, 11 | }, 12 | select: { 13 | userId: true, 14 | }, 15 | }); 16 | if (!comment) { 17 | return { 18 | ok: false, 19 | error: "Comment not found.", 20 | }; 21 | } else if (comment.userId !== loggedInUser.id) { 22 | return { 23 | ok: false, 24 | error: "Not authorized.", 25 | }; 26 | } else { 27 | await client.comment.update({ 28 | where: { 29 | id, 30 | }, 31 | data: { 32 | payload, 33 | }, 34 | }); 35 | return { 36 | ok: true, 37 | }; 38 | } 39 | } 40 | ), 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/comments/createComment/createComment.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | createComment: protectedResolver( 7 | async (_, { photoId, payload }, { loggedInUser }) => { 8 | const ok = await client.photo.findUnique({ 9 | where: { 10 | id: photoId, 11 | }, 12 | select: { 13 | id: true, 14 | }, 15 | }); 16 | if (!ok) { 17 | return { 18 | ok: false, 19 | error: "Photo not found.", 20 | }; 21 | } 22 | const newComment = await client.comment.create({ 23 | data: { 24 | payload, 25 | photo: { 26 | connect: { 27 | id: photoId, 28 | }, 29 | }, 30 | user: { 31 | connect: { 32 | id: loggedInUser.id, 33 | }, 34 | }, 35 | }, 36 | }); 37 | return { 38 | ok: true, 39 | id: newComment.id, 40 | }; 41 | } 42 | ), 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/users/createAccount/createAccount.resolvers.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import client from "../../client"; 3 | 4 | export default { 5 | Mutation: { 6 | createAccount: async ( 7 | _, 8 | { firstName, lastName, username, email, password } 9 | ) => { 10 | try { 11 | const existingUser = await client.user.findFirst({ 12 | where: { 13 | OR: [ 14 | { 15 | username, 16 | }, 17 | { 18 | email, 19 | }, 20 | ], 21 | }, 22 | }); 23 | if (existingUser) { 24 | throw new Error("This username/password is already taken."); 25 | } 26 | const uglyPassword = await bcrypt.hash(password, 10); 27 | await client.user.create({ 28 | data: { 29 | username, 30 | email, 31 | firstName, 32 | lastName, 33 | password: uglyPassword, 34 | }, 35 | }); 36 | return { 37 | ok: true, 38 | }; 39 | } catch (e) { 40 | return { 41 | ok: false, 42 | error: "Cant create account.", 43 | }; 44 | } 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/users/users.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../client"; 2 | 3 | export default { 4 | User: { 5 | totalFollowing: ({ id }) => 6 | client.user.count({ 7 | where: { 8 | followers: { 9 | some: { 10 | id, 11 | }, 12 | }, 13 | }, 14 | }), 15 | totalFollowers: ({ id }) => 16 | client.user.count({ 17 | where: { 18 | following: { 19 | some: { 20 | id, 21 | }, 22 | }, 23 | }, 24 | }), 25 | isMe: ({ id }, _, { loggedInUser }) => { 26 | if (!loggedInUser) { 27 | return false; 28 | } 29 | return id === loggedInUser.id; 30 | }, 31 | isFollowing: async ({ id }, _, { loggedInUser }) => { 32 | if (!loggedInUser) { 33 | return false; 34 | } 35 | const exists = await client.user.count({ 36 | where: { 37 | username: loggedInUser.username, 38 | following: { 39 | some: { 40 | id, 41 | }, 42 | }, 43 | }, 44 | }); 45 | return Boolean(exists); 46 | }, 47 | photos: ({ id }) => client.user.findUnique({ where: { id } }).photos(), 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/photos/editPhoto/editPhoto.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | import { processHashtags } from "../photos.utils"; 4 | 5 | export default { 6 | Mutation: { 7 | editPhoto: protectedResolver( 8 | async (_, { id, caption }, { loggedInUser }) => { 9 | const oldPhoto = await client.photo.findFirst({ 10 | where: { 11 | id, 12 | userId: loggedInUser.id, 13 | }, 14 | include: { 15 | hashtags: { 16 | select: { 17 | hashtag: true, 18 | }, 19 | }, 20 | }, 21 | }); 22 | if (!oldPhoto) { 23 | return { 24 | ok: false, 25 | error: "Photo not found.", 26 | }; 27 | } 28 | await client.photo.update({ 29 | where: { 30 | id, 31 | }, 32 | data: { 33 | caption, 34 | hashtags: { 35 | disconnect: oldPhoto.hashtags, 36 | connectOrCreate: processHashtags(caption), 37 | }, 38 | }, 39 | }); 40 | return { 41 | ok: true, 42 | }; 43 | } 44 | ), 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/photos/toggleLike/toggleLike.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { protectedResolver } from "../../users/users.utils"; 3 | 4 | export default { 5 | Mutation: { 6 | toggleLike: protectedResolver(async (_, { id }, { loggedInUser }) => { 7 | const photo = await client.photo.findUnique({ 8 | where: { 9 | id, 10 | }, 11 | }); 12 | if (!photo) { 13 | return { 14 | ok: false, 15 | error: "Photo not found", 16 | }; 17 | } 18 | const likeWhere = { 19 | photoId_userId: { 20 | userId: loggedInUser.id, 21 | photoId: id, 22 | }, 23 | }; 24 | const like = await client.like.findUnique({ 25 | where: likeWhere, 26 | }); 27 | if (like) { 28 | await client.like.delete({ 29 | where: likeWhere, 30 | }); 31 | } else { 32 | await client.like.create({ 33 | data: { 34 | user: { 35 | connect: { 36 | id: loggedInUser.id, 37 | }, 38 | }, 39 | photo: { 40 | connect: { 41 | id: photo.id, 42 | }, 43 | }, 44 | }, 45 | }); 46 | } 47 | return { 48 | ok: true, 49 | }; 50 | }), 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /prisma/migrations/20210204061820_photos/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Photo" ( 3 | "id" SERIAL NOT NULL, 4 | "userId" INTEGER NOT NULL, 5 | "file" TEXT NOT NULL, 6 | "caption" TEXT, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | 10 | PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "Hashtag" ( 15 | "id" SERIAL NOT NULL, 16 | "hashtag" TEXT NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "updatedAt" TIMESTAMP(3) NOT NULL, 19 | 20 | PRIMARY KEY ("id") 21 | ); 22 | 23 | -- CreateTable 24 | CREATE TABLE "_HashtagToPhoto" ( 25 | "A" INTEGER NOT NULL, 26 | "B" INTEGER NOT NULL 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "_HashtagToPhoto_AB_unique" ON "_HashtagToPhoto"("A", "B"); 31 | 32 | -- CreateIndex 33 | CREATE INDEX "_HashtagToPhoto_B_index" ON "_HashtagToPhoto"("B"); 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "Photo" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 37 | 38 | -- AddForeignKey 39 | ALTER TABLE "_HashtagToPhoto" ADD FOREIGN KEY ("A") REFERENCES "Hashtag"("id") ON DELETE CASCADE ON UPDATE CASCADE; 40 | 41 | -- AddForeignKey 42 | ALTER TABLE "_HashtagToPhoto" ADD FOREIGN KEY ("B") REFERENCES "Photo"("id") ON DELETE CASCADE ON UPDATE CASCADE; 43 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import http from "http"; 3 | import express from "express"; 4 | import logger from "morgan"; 5 | import { ApolloServer } from "apollo-server-express"; 6 | import { typeDefs, resolvers } from "./schema"; 7 | import { getUser } from "./users/users.utils"; 8 | 9 | const PORT = process.env.PORT; 10 | const apollo = new ApolloServer({ 11 | resolvers, 12 | typeDefs, 13 | context: async (ctx) => { 14 | if (ctx.req) { 15 | return { 16 | loggedInUser: await getUser(ctx.req.headers.token), 17 | }; 18 | } else { 19 | const { 20 | connection: { context }, 21 | } = ctx; 22 | return { 23 | loggedInUser: context.loggedInUser, 24 | }; 25 | } 26 | }, 27 | subscriptions: { 28 | onConnect: async ({ token }) => { 29 | if (!token) { 30 | throw new Error("You can't listen."); 31 | } 32 | const loggedInUser = await getUser(token); 33 | return { 34 | loggedInUser, 35 | }; 36 | }, 37 | }, 38 | }); 39 | 40 | const app = express(); 41 | app.use(logger("dev")); 42 | apollo.applyMiddleware({ app }); 43 | app.use("/static", express.static("uploads")); 44 | 45 | const httpServer = http.createServer(app); 46 | apollo.installSubscriptionHandlers(httpServer); 47 | 48 | httpServer.listen(PORT, () => { 49 | console.log(`🚀Server is running on http://localhost:${PORT} ✅`); 50 | }); 51 | -------------------------------------------------------------------------------- /prisma/migrations/20210210064620_messages/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Room" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | 7 | PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "Message" ( 12 | "id" SERIAL NOT NULL, 13 | "payload" TEXT NOT NULL, 14 | "userId" INTEGER NOT NULL, 15 | "roomId" INTEGER NOT NULL, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL, 18 | 19 | PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "_RoomToUser" ( 24 | "A" INTEGER NOT NULL, 25 | "B" INTEGER NOT NULL 26 | ); 27 | 28 | -- CreateIndex 29 | CREATE UNIQUE INDEX "_RoomToUser_AB_unique" ON "_RoomToUser"("A", "B"); 30 | 31 | -- CreateIndex 32 | CREATE INDEX "_RoomToUser_B_index" ON "_RoomToUser"("B"); 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "Message" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 36 | 37 | -- AddForeignKey 38 | ALTER TABLE "Message" ADD FOREIGN KEY ("roomId") REFERENCES "Room"("id") ON DELETE CASCADE ON UPDATE CASCADE; 39 | 40 | -- AddForeignKey 41 | ALTER TABLE "_RoomToUser" ADD FOREIGN KEY ("A") REFERENCES "Room"("id") ON DELETE CASCADE ON UPDATE CASCADE; 42 | 43 | -- AddForeignKey 44 | ALTER TABLE "_RoomToUser" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 45 | -------------------------------------------------------------------------------- /src/messages/roomUpdates/roomUpdates.resolvers.js: -------------------------------------------------------------------------------- 1 | import { withFilter } from "apollo-server"; 2 | import client from "../../client"; 3 | import { NEW_MESSAGE } from "../../constants"; 4 | import pubsub from "../../pubsub"; 5 | 6 | export default { 7 | Subscription: { 8 | roomUpdates: { 9 | subscribe: async (root, args, context, info) => { 10 | const room = await client.room.findFirst({ 11 | where: { 12 | id: args.id, 13 | users: { 14 | some: { 15 | id: context.loggedInUser.id, 16 | }, 17 | }, 18 | }, 19 | select: { 20 | id: true, 21 | }, 22 | }); 23 | if (!room) { 24 | throw new Error("You shall not see this."); 25 | } 26 | return withFilter( 27 | () => pubsub.asyncIterator(NEW_MESSAGE), 28 | async ({ roomUpdates }, { id }, { loggedInUser }) => { 29 | if (roomUpdates.roomId === id) { 30 | const room = await client.room.findFirst({ 31 | where: { 32 | id, 33 | users: { 34 | some: { 35 | id: loggedInUser.id, 36 | }, 37 | }, 38 | }, 39 | select: { 40 | id: true, 41 | }, 42 | }); 43 | if (!room) { 44 | return false; 45 | } 46 | return true; 47 | } 48 | } 49 | )(root, args, context, info); 50 | }, 51 | }, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instaclone", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "preinstall": "npx npm-force-resolutions", 7 | "dev": "nodemon --exec babel-node src/server --delay 2", 8 | "migrate": "npx prisma migrate dev --preview-feature", 9 | "studio": "npx prisma studio", 10 | "build": "babel src --out-dir build", 11 | "start": "node build/server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/nomadcoders/instaclone-backend.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/nomadcoders/instaclone-backend/issues" 22 | }, 23 | "homepage": "https://github.com/nomadcoders/instaclone-backend#readme", 24 | "dependencies": { 25 | "@babel/cli": "^7.13.10", 26 | "@prisma/client": "^2.16.1", 27 | "apollo-server": "^2.19.1", 28 | "apollo-server-express": "^2.19.2", 29 | "aws-sdk": "^2.839.0", 30 | "bcrypt": "^5.0.0", 31 | "dotenv": "^8.2.0", 32 | "express": "^4.17.1", 33 | "graphql": "^15.4.0", 34 | "graphql-tools": "^7.0.2", 35 | "jsonwebtoken": "^8.5.1", 36 | "morgan": "^1.10.0", 37 | "fs-capacitor": "^6.2.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.12.10", 41 | "@babel/node": "^7.12.10", 42 | "@babel/plugin-transform-regenerator": "^7.12.13", 43 | "@babel/plugin-transform-runtime": "^7.13.10", 44 | "@babel/preset-env": "^7.12.11", 45 | "@prisma/cli": "^2.15.0", 46 | "nodemon": "^2.0.7", 47 | "prisma": "^2.16.1" 48 | }, 49 | "resolutions": { 50 | "fs-capacitor": "^6.2.0", 51 | "graphql-upload": "^11.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/users/editProfile/editProfile.resolvers.js: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from "fs"; 2 | import bcrypt from "bcrypt"; 3 | import client from "../../client"; 4 | import { protectedResolver } from "../users.utils"; 5 | import { uploadToS3 } from "../../shared/shared.utils"; 6 | 7 | const resolverFn = async ( 8 | _, 9 | { firstName, lastName, username, email, password: newPassword, bio, avatar }, 10 | { loggedInUser } 11 | ) => { 12 | let avatarUrl = null; 13 | if (avatar) { 14 | avatarUrl = await uploadToS3(avatar, loggedInUser.id, "avatars"); 15 | /* const { filename, createReadStream } = await avatar; 16 | const newFilename = `${loggedInUser.id}-${Date.now()}-${filename}`; 17 | const readStream = createReadStream(); 18 | const writeStream = createWriteStream( 19 | process.cwd() + "/uploads/" + newFilename 20 | ); 21 | readStream.pipe(writeStream); 22 | avatarUrl = `http://localhost:4000/static/${newFilename}`; */ 23 | } 24 | let uglyPassword = null; 25 | if (newPassword) { 26 | uglyPassword = await bcrypt.hash(newPassword, 10); 27 | } 28 | const updatedUser = await client.user.update({ 29 | where: { 30 | id: loggedInUser.id, 31 | }, 32 | data: { 33 | firstName, 34 | lastName, 35 | username, 36 | email, 37 | bio, 38 | ...(uglyPassword && { password: uglyPassword }), 39 | ...(avatarUrl && { avatar: avatarUrl }), 40 | }, 41 | }); 42 | if (updatedUser.id) { 43 | return { 44 | ok: true, 45 | }; 46 | } else { 47 | return { 48 | ok: false, 49 | error: "Could not update profile.", 50 | }; 51 | } 52 | }; 53 | 54 | export default { 55 | Mutation: { 56 | editProfile: protectedResolver(resolverFn), 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/photos/photos.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../client"; 2 | 3 | export default { 4 | Photo: { 5 | user: ({ userId }) => client.user.findUnique({ where: { id: userId } }), 6 | hashtags: ({ id }) => 7 | client.hashtag.findMany({ 8 | where: { 9 | photos: { 10 | some: { 11 | id, 12 | }, 13 | }, 14 | }, 15 | }), 16 | likes: ({ id }) => client.like.count({ where: { photoId: id } }), 17 | commentNumber: ({ id }) => client.comment.count({ where: { photoId: id } }), 18 | comments: ({ id }) => 19 | client.comment.findMany({ 20 | where: { photoId: id }, 21 | include: { 22 | user: true, 23 | }, 24 | }), 25 | isMine: ({ userId }, _, { loggedInUser }) => { 26 | if (!loggedInUser) { 27 | return false; 28 | } 29 | return userId === loggedInUser.id; 30 | }, 31 | isLiked: async ({ id }, _, { loggedInUser }) => { 32 | if (!loggedInUser) { 33 | return false; 34 | } 35 | const ok = await client.like.findUnique({ 36 | where: { 37 | photoId_userId: { 38 | photoId: id, 39 | userId: loggedInUser.id, 40 | }, 41 | }, 42 | select: { 43 | id: true, 44 | }, 45 | }); 46 | if (ok) { 47 | return true; 48 | } 49 | return false; 50 | }, 51 | }, 52 | Hashtag: { 53 | photos: ({ id }, { page }, { loggedInUser }) => { 54 | return client.hashtag 55 | .findUnique({ 56 | where: { 57 | id, 58 | }, 59 | }) 60 | .photos(); 61 | }, 62 | totalPhotos: ({ id }) => 63 | client.photo.count({ 64 | where: { 65 | hashtags: { 66 | some: { 67 | id, 68 | }, 69 | }, 70 | }, 71 | }), 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /src/messages/sendMessage/sendMessage.resolvers.js: -------------------------------------------------------------------------------- 1 | import client from "../../client"; 2 | import { NEW_MESSAGE } from "../../constants"; 3 | import pubsub from "../../pubsub"; 4 | import { protectedResolver } from "../../users/users.utils"; 5 | 6 | export default { 7 | Mutation: { 8 | sendMessage: protectedResolver( 9 | async (_, { payload, roomId, userId }, { loggedInUser }) => { 10 | let room = null; 11 | if (userId) { 12 | const user = await client.user.findUnique({ 13 | where: { 14 | id: userId, 15 | }, 16 | select: { 17 | id: true, 18 | }, 19 | }); 20 | if (!user) { 21 | return { 22 | ok: false, 23 | error: "This user does not exist.", 24 | }; 25 | } 26 | room = await client.room.create({ 27 | data: { 28 | users: { 29 | connect: [ 30 | { 31 | id: userId, 32 | }, 33 | { 34 | id: loggedInUser.id, 35 | }, 36 | ], 37 | }, 38 | }, 39 | }); 40 | } else if (roomId) { 41 | room = await client.room.findUnique({ 42 | where: { 43 | id: roomId, 44 | }, 45 | select: { 46 | id: true, 47 | }, 48 | }); 49 | if (!room) { 50 | return { 51 | ok: false, 52 | error: "Room not found.", 53 | }; 54 | } 55 | } 56 | const message = await client.message.create({ 57 | data: { 58 | payload, 59 | room: { 60 | connect: { 61 | id: room.id, 62 | }, 63 | }, 64 | user: { 65 | connect: { 66 | id: loggedInUser.id, 67 | }, 68 | }, 69 | }, 70 | }); 71 | console.log(message); 72 | pubsub.publish(NEW_MESSAGE, { roomUpdates: { ...message } }); 73 | return { 74 | ok: true, 75 | id: message.id, 76 | }; 77 | } 78 | ), 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | /build -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model User { 14 | id Int @id @default(autoincrement()) 15 | firstName String 16 | lastName String? 17 | username String @unique 18 | email String @unique 19 | password String 20 | bio String? 21 | avatar String? 22 | photos Photo[] 23 | likes Like[] 24 | followers User[] @relation("FollowRelation", references: [id]) 25 | following User[] @relation("FollowRelation", references: [id]) 26 | comments Comment[] 27 | rooms Room[] 28 | createdAt DateTime @default(now()) 29 | updatedAt DateTime @updatedAt 30 | Message Message[] 31 | } 32 | 33 | model Photo { 34 | id Int @id @default(autoincrement()) 35 | user User @relation(fields: [userId], references: [id]) 36 | userId Int 37 | file String 38 | caption String? 39 | hashtags Hashtag[] 40 | createdAt DateTime @default(now()) 41 | updatedAt DateTime @updatedAt 42 | likes Like[] 43 | comments Comment[] 44 | } 45 | 46 | model Hashtag { 47 | id Int @id @default(autoincrement()) 48 | hashtag String @unique 49 | photos Photo[] 50 | createdAt DateTime @default(now()) 51 | updatedAt DateTime @updatedAt 52 | } 53 | 54 | model Like { 55 | id Int @id @default(autoincrement()) 56 | photo Photo @relation(fields: [photoId], references: [id]) 57 | user User @relation(fields: [userId], references: [id]) 58 | photoId Int 59 | userId Int 60 | createdAt DateTime @default(now()) 61 | updatedAt DateTime @updatedAt 62 | 63 | @@unique([photoId, userId]) 64 | } 65 | 66 | model Comment { 67 | id Int @id @default(autoincrement()) 68 | user User @relation(fields: [userId], references: [id]) 69 | photo Photo @relation(fields: [photoId], references: [id]) 70 | payload String 71 | createdAt DateTime @default(now()) 72 | updatedAt DateTime @updatedAt 73 | userId Int 74 | photoId Int 75 | } 76 | 77 | model Room { 78 | id Int @id @default(autoincrement()) 79 | users User[] 80 | messages Message[] 81 | createdAt DateTime @default(now()) 82 | updatedAt DateTime @updatedAt 83 | } 84 | 85 | model Message { 86 | id Int @id @default(autoincrement()) 87 | payload String 88 | user User @relation(fields: [userId], references: [id]) 89 | userId Int 90 | room Room @relation(fields: [roomId], references: [id]) 91 | roomId Int 92 | read Boolean @default(false) 93 | createdAt DateTime @default(now()) 94 | updatedAt DateTime @updatedAt 95 | } 96 | --------------------------------------------------------------------------------