diff --git a/graphql/queryResolvers/lessons.test.js b/graphql/queryResolvers/lessons.test.js index a9050a484..18e0faab7 100644 --- a/graphql/queryResolvers/lessons.test.js +++ b/graphql/queryResolvers/lessons.test.js @@ -1,11 +1,9 @@ +import { prisma } from '../../prisma' import { lessons } from './lessons' -import db from '../../helpers/dbload' describe('Lessons resolver', () => { - const { Lesson } = db - test('lessons should return an empty array', async () => { - Lesson.findAll = jest.fn().mockReturnValue([]) + prisma.lesson.findMany = jest.fn().mockReturnValue([]) expect(lessons()).toEqual([]) }) }) diff --git a/graphql/queryResolvers/lessons.ts b/graphql/queryResolvers/lessons.ts index c025d3caa..d3177fdc3 100644 --- a/graphql/queryResolvers/lessons.ts +++ b/graphql/queryResolvers/lessons.ts @@ -1,13 +1,10 @@ -import db from '../../helpers/dbload' - -const { Lesson } = db +import { prisma } from '../../prisma' export const lessons = () => { - return Lesson.findAll({ - include: ['challenges'], - order: [ - ['order', 'ASC'], - ['challenges', 'order', 'ASC'] - ] + return prisma.lesson.findMany({ + include: { challenges: { orderBy: { order: 'asc' } } }, + orderBy: { + order: 'asc' + } }) } diff --git a/helpers/controllers/authController.test.js b/helpers/controllers/authController.test.js index a7b56a4e0..030ca806f 100644 --- a/helpers/controllers/authController.test.js +++ b/helpers/controllers/authController.test.js @@ -7,6 +7,7 @@ import bcrypt from 'bcrypt' import db from '../dbload' import { login, logout, signup, isTokenValid } from './authController' import { chatSignUp } from '../mattermost' +import { prisma } from '../../prisma' describe('auth controller', () => { let userArgs @@ -30,14 +31,14 @@ describe('auth controller', () => { }) test('Login - should throw error if user cannot be found', async () => { - db.User.findOne = jest.fn().mockReturnValue(null) + prisma.user.findFirst = jest.fn().mockReturnValue(null) return expect( login({}, userArgs, { req: { session: {} } }) ).rejects.toThrowError('User does not exist') }) test('Login - should throw error if password is invalid', async () => { - db.User.findOne = jest.fn().mockReturnValue({}) + prisma.user.findFirst = jest.fn().mockReturnValue({}) bcrypt.compare = jest.fn().mockReturnValue(false) return expect( login({}, userArgs, { req: { session: {} } }) @@ -45,9 +46,11 @@ describe('auth controller', () => { }) test('Login - should return success true if successful login', async () => { - db.User.findOne = jest - .fn() - .mockReturnValue({ username: 'testuser', cliToken: 'fakeCliToken' }) + prisma.user.findFirst = jest.fn().mockReturnValue({ + username: 'testuser', + password: 'fakepassword', + cliToken: 'fakeCliToken' + }) bcrypt.compare = jest.fn().mockReturnValue(true) const result = await login({}, userArgs, { req: { session: {} } }) expect(result).toEqual({ @@ -58,10 +61,10 @@ describe('auth controller', () => { }) test('Login - should return user with a new CLI token', async () => { - db.User.findOne = jest.fn().mockResolvedValue({ - username: 'fakeUser', - update: obj => jest.fn().mockReturnThis(obj) - }) + prisma.user.findFirst = jest + .fn() + .mockReturnValue({ username: 'fakeUser', password: 'fakePassword' }) + prisma.user.update = obj => jest.fn().mockReturnThis(obj) bcrypt.compare = jest.fn().mockReturnValue(true) const result = await login({}, userArgs, { req: { session: {} } }) expect(result.cliToken).toBeTruthy() diff --git a/helpers/controllers/authController.ts b/helpers/controllers/authController.ts index 9cd7805c6..91d13db75 100644 --- a/helpers/controllers/authController.ts +++ b/helpers/controllers/authController.ts @@ -7,6 +7,7 @@ import { chatSignUp } from '../mattermost' import { Context } from '../../@types/helpers' import { encode, decode } from '../encoding' import { sendSignupEmail } from '../mail' +import { prisma } from '../../prisma' const { User } = db const THREE_DAYS = 1000 * 60 * 60 * 24 * 3 @@ -34,17 +35,28 @@ export const login = async (_parent: void, arg: Login, ctx: Context) => { throw new Error('Session Error') } - const user = await User.findOne({ where: { username } }) + const user = await prisma.user.findFirst({ where: { username } }) + // TODO change username column to be unique + // const user = await prisma.user.findUnique({ where: { username } }) if (!user) { throw new UserInputError('User does not exist') } - const validLogin = await bcrypt.compare(password, user.password) + const validLogin = user.password + ? await bcrypt.compare(password, user.password) + : false if (!validLogin) { throw new AuthenticationError('Password is invalid') } - if (!user.cliToken) await user.update({ cliToken: nanoid() }) + if (!user.cliToken) { + await prisma.user.update({ + where: { + id: user.id + }, + data: { cliToken: nanoid() } + }) + } const cliToken = { id: user.id, cliToken: user.cliToken } diff --git a/package.json b/package.json index c3fbd94ac..6c57cbc06 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "build-storybook": "build-storybook -c .storybook -s ./public", "test": "jest --coverage", "type-check": "tsc", - "generate": "graphql-codegen --config codegen.yml" + "generate": "graphql-codegen --config codegen.yml", + "postinstall": "prisma generate" }, "dependencies": { "@apollo/client": "^3.3.11", + "@prisma/client": "2.19.0", "@sentry/browser": "^5.30.0", "@zeit/next-sass": "^1.0.1", "apollo-server-micro": "^2.20.0", @@ -94,6 +96,7 @@ "husky": "^4.3.7", "jest": "^26.6.3", "prettier": "^2.2.1", + "prisma": "2.19.0", "react-test-renderer": "^17.0.1", "sass-loader": "^10.0.1", "svgo": "^1.3.2", @@ -115,7 +118,8 @@ "coveragePathIgnorePatterns": [ "stories", "graphql/index.tsx", - "/__tests__/utils/" + "/__tests__/utils/", + "prisma" ], "testPathIgnorePatterns": [ "/__tests__/utils/" diff --git a/prisma/index.ts b/prisma/index.ts new file mode 100644 index 000000000..29ede7fd5 --- /dev/null +++ b/prisma/index.ts @@ -0,0 +1,28 @@ +import { PrismaClient } from '@prisma/client' + +declare global { + // eslint-disable-next-line no-var + var prismag: PrismaClient +} + +const prismaOptions = { + datasources: { + db: { + url: `postgresql://${process.env.DB_USER}:${process.env.DB_PW}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` + } + } +} + +// Avoid instantiating too many instances of Prisma in development +// https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#problem +export let prisma: PrismaClient + +// check to use this workaround only in development and not in production +if (process.env.NODE_ENV === 'production') { + prisma = new PrismaClient(prismaOptions) +} else { + if (!global.prismag) { + global.prismag = new PrismaClient(prismaOptions) + } + prisma = global.prismag +} diff --git a/prisma/migrations/20210323001623_init/migration.sql b/prisma/migrations/20210323001623_init/migration.sql new file mode 100644 index 000000000..13c34c48e --- /dev/null +++ b/prisma/migrations/20210323001623_init/migration.sql @@ -0,0 +1,144 @@ +-- CreateTable +CREATE TABLE "Session" ( + "sid" VARCHAR(36) NOT NULL, + "expires" TIMESTAMPTZ(6), + "data" TEXT, + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + + PRIMARY KEY ("sid") +); + +-- CreateTable +CREATE TABLE "alerts" ( + "id" SERIAL NOT NULL, + "text" VARCHAR(255), + "type" VARCHAR(255), + "url" VARCHAR(255), + "urlCaption" VARCHAR(255), + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "challenges" ( + "id" SERIAL NOT NULL, + "status" VARCHAR(255), + "description" TEXT, + "title" VARCHAR(255), + "order" INTEGER, + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + "lessonId" INTEGER, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "lessons" ( + "id" SERIAL NOT NULL, + "description" TEXT, + "docUrl" VARCHAR(255), + "githubUrl" VARCHAR(255), + "videoUrl" VARCHAR(255), + "order" INTEGER, + "title" VARCHAR(255), + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + "chatUrl" VARCHAR(255), + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "stars" ( + "id" SERIAL NOT NULL, + "lessonId" INTEGER, + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + "studentId" INTEGER, + "mentorId" INTEGER, + "comment" VARCHAR(255), + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "submissions" ( + "id" SERIAL NOT NULL, + "mrUrl" VARCHAR(255), + "diff" TEXT, + "comment" TEXT, + "status" VARCHAR(255), + "viewCount" INTEGER DEFAULT 0, + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + "userId" INTEGER, + "reviewerId" INTEGER, + "challengeId" INTEGER, + "lessonId" INTEGER, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "userLessons" ( + "isPassed" VARCHAR(255), + "isTeaching" VARCHAR(255), + "isEnrolled" VARCHAR(255), + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + "lessonId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + + PRIMARY KEY ("lessonId","userId") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "name" VARCHAR(255), + "username" VARCHAR(255), + "password" VARCHAR(255), + "email" VARCHAR(255), + "gsId" INTEGER, + "isOnline" BOOLEAN, + "createdAt" TIMESTAMPTZ(6) NOT NULL, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + "isAdmin" VARCHAR(255) DEFAULT false, + "forgotToken" VARCHAR(255), + "cliToken" VARCHAR(255), + "emailVerificationToken" VARCHAR(255), + "tokenExpiration" TIMESTAMPTZ(6), + + PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "challenges" ADD FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "stars" ADD FOREIGN KEY ("mentorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "stars" ADD FOREIGN KEY ("studentId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "submissions" ADD FOREIGN KEY ("challengeId") REFERENCES "challenges"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "submissions" ADD FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "submissions" ADD FOREIGN KEY ("reviewerId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "submissions" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "userLessons" ADD FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "userLessons" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20210323001806_star_lesson_foreignkey/migration.sql b/prisma/migrations/20210323001806_star_lesson_foreignkey/migration.sql new file mode 100644 index 000000000..be5635884 --- /dev/null +++ b/prisma/migrations/20210323001806_star_lesson_foreignkey/migration.sql @@ -0,0 +1,2 @@ +-- AddForeignKey +ALTER TABLE "stars" ADD FOREIGN KEY ("lessonId") REFERENCES "lessons"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20210323140004_user_unique_username/migration.sql b/prisma/migrations/20210323140004_user_unique_username/migration.sql new file mode 100644 index 000000000..ca53b1ec7 --- /dev/null +++ b/prisma/migrations/20210323140004_user_unique_username/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - The migration will add a unique constraint covering the columns `[username]` on the table `users`. If there are existing duplicate values, the migration will fail. + - Made the column `username` on table `users` required. The migration will fail if there are existing NULL values in that column. + +*/ +-- Delete duplicate usernames while keeping the latest entry +DELETE FROM "users" A USING "users" B +WHERE A.username = B.username AND A.id < B.id; + +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "username" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "users.username_unique" ON "users"("username"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 000000000..14a2a97af --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,136 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["orderByRelation"] +} + +datasource db { + provider = "postgresql" + url = env("DB_URL") +} + +model Session { + sid String @id @db.VarChar(36) + expires DateTime? @db.Timestamptz(6) + data String? + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) +} + +model Alert { + id Int @id @default(autoincrement()) + text String? @db.VarChar(255) + type String? @db.VarChar(255) + url String? @db.VarChar(255) + urlCaption String? @db.VarChar(255) + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + + @@map("alerts") +} + +model Challenge { + id Int @id @default(autoincrement()) + status String? @db.VarChar(255) + description String? + title String? @db.VarChar(255) + order Int? + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + lessonId Int? + lesson Lesson? @relation(fields: [lessonId], references: [id]) + submissions Submission[] + + @@map("challenges") +} + +model Lesson { + id Int @id @default(autoincrement()) + description String? + docUrl String? @db.VarChar(255) + githubUrl String? @db.VarChar(255) + videoUrl String? @db.VarChar(255) + order Int? + title String? @db.VarChar(255) + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + chatUrl String? @db.VarChar(255) + challenges Challenge[] + submissions Submission[] + userLessons UserLesson[] + + @@map("lessons") +} + +model Star { + id Int @id @default(autoincrement()) + lessonId Int? + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + studentId Int? + mentorId Int? + comment String? @db.VarChar(255) + mentor User? @relation("starMentor", fields: [mentorId], references: [id]) + student User? @relation("starStudent", fields: [studentId], references: [id]) + + @@map("stars") +} + +model Submission { + id Int @id @default(autoincrement()) + mrUrl String? @db.VarChar(255) + diff String? + comment String? + status String? @db.VarChar(255) + viewCount Int? @default(0) + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + userId Int? + reviewerId Int? + challengeId Int? + lessonId Int? + challenge Challenge? @relation(fields: [challengeId], references: [id]) + lesson Lesson? @relation(fields: [lessonId], references: [id]) + reviewer User? @relation("userReviewedSubmissions", fields: [reviewerId], references: [id]) + user User? @relation("userSubmissions", fields: [userId], references: [id]) + + @@map("submissions") +} + +model UserLesson { + isPassed String? @db.VarChar(255) + isTeaching String? @db.VarChar(255) + isEnrolled String? @db.VarChar(255) + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + lessonId Int + userId Int + lesson Lesson @relation(fields: [lessonId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@id([lessonId, userId]) + @@map("userLessons") +} + +model User { + id Int @id @default(autoincrement()) + name String? @db.VarChar(255) + username String? @db.VarChar(255) + password String? @db.VarChar(255) + email String? @db.VarChar(255) + gsId Int? + isOnline Boolean? + createdAt DateTime @db.Timestamptz(6) + updatedAt DateTime @db.Timestamptz(6) + isAdmin String? @default(dbgenerated("false")) @db.VarChar(255) + forgotToken String? @db.VarChar(255) + cliToken String? @db.VarChar(255) + emailVerificationToken String? @db.VarChar(255) + tokenExpiration DateTime? @db.Timestamptz(6) + starsMentor Star[] @relation("starMentor") + starsGiven Star[] @relation("starStudent") + submissionsReviewed Submission[] @relation("userReviewedSubmissions") + submissions Submission[] @relation("userSubmissions") + userLessons UserLesson[] + + @@map("users") +} diff --git a/yarn.lock b/yarn.lock index 6f443989e..278d501ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2157,6 +2157,23 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.1.tgz#7f554e7368c9ab679a11f4a042ca17149d70cf12" integrity sha512-DvJbbn3dUgMxDnJLH+RZQPnXak1h4ZVYQ7CWiFWjQwBFkVajT4rfw2PdpHLTSTwxrYfnoEXkuBiwkDm6tPMQeA== +"@prisma/client@2.19.0": + version "2.19.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.19.0.tgz#a45f17a59fd109e95b61bf4b56d4a7642169ec0e" + integrity sha512-QK4M8TjJh1QesyO9aLM7DeAQUi5+UnNHpEAm5kwqBO1cq/4Ag5yU9ladctJFJleEE5BLewXHwV2t9A+VfCZslg== + dependencies: + "@prisma/engines-version" "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d" + +"@prisma/engines-version@2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d": + version "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d.tgz#a7f80d481ec6cb8e2975ab530664d4ca5fc9eba6" + integrity sha512-NzhbwC4iMbRQwJxdhNQX6eaVcOuNGtHRk6aesWE4KMf/YmlW5kfi3HDy7WZ/C4P0Iyn9oURDuk+xZV6QDUVjTw== + +"@prisma/engines@2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d": + version "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d.tgz#db2809a6f7f18584e3ca89b1f5bad884155629ec" + integrity sha512-rEWpaG7wZvPuWJC5SwkBB/Iwue//oC5yv58Mse7r+ibtgkA7vGdWc1bFDQ32DT9tDL5WSC6bBwqEASGV/1Gm1Q== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -12349,6 +12366,13 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= +prisma@2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.19.0.tgz#2c14f9cbbfb0ab69c8a9473e16736759713d29ad" + integrity sha512-iartCNVrtR4XT20ABN3zrSi3R/pCBe75Y0ZH8681QIGm8qjRQzf3DnbscPZgZ9iY4KFuVxL8ZrBQVDmRhpN0EQ== + dependencies: + "@prisma/engines" "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d" + prismjs@^1.21.0, prismjs@^1.23.0, prismjs@~1.23.0: version "1.23.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"