diff --git a/packages/api/db/migrations/20200608182453_create_users_table.ts b/packages/api/db/migrations/20200608182453_create_users_table.ts index 8b1cc1c..a3b11be 100644 --- a/packages/api/db/migrations/20200608182453_create_users_table.ts +++ b/packages/api/db/migrations/20200608182453_create_users_table.ts @@ -7,8 +7,9 @@ export const up = async (knex: Knex) => { table.string("email").notNullable(); table.string("password").notNullable(); table.string("role").notNullable(); - table.integer("rank").notNullable(); + table.integer("exp").notNullable(); table.string("description"); + table.specificType("tutorials", "INT[]").notNullable(); }); }; diff --git a/packages/api/db/migrations/20200707125043_create_texts_table.ts b/packages/api/db/migrations/20200707125043_create_texts_table.ts index e0acdc1..3ff49f6 100644 --- a/packages/api/db/migrations/20200707125043_create_texts_table.ts +++ b/packages/api/db/migrations/20200707125043_create_texts_table.ts @@ -5,6 +5,9 @@ export const up = async (knex: Knex) => { table.increments("id"); table.string("text").notNullable(); table.boolean("ordered"); + table.boolean("tutorial"); + table.integer("difficulty").notNullable(); + table.integer("author").notNullable(); }); }; diff --git a/packages/api/db/seeds/examples/users.ts b/packages/api/db/seeds/examples/users.ts index 9f8e1d9..8e5cbdd 100644 --- a/packages/api/db/seeds/examples/users.ts +++ b/packages/api/db/seeds/examples/users.ts @@ -4,7 +4,8 @@ interface Users { password: string; role?: string | null; description?: string | null; - rank: number; + exp: number; + tutorials: number[]; } export default [ @@ -14,7 +15,8 @@ export default [ password: "UserPass", role: "admin", description: null, - rank: 8 + exp: 8, + tutorials: [] }, { name: "MKGUN3", @@ -22,7 +24,8 @@ export default [ password: "MaoGay", role: "member", description: null, - rank: 4 + exp: 4, + tutorials: [] }, { name: "NotAUser", @@ -30,7 +33,8 @@ export default [ password: "nothing", role: "member", description: null, - rank: 2 + exp: 2, + tutorials: [] }, { name: "Guy2", @@ -38,6 +42,7 @@ export default [ password: "JustAGuy", role: "member", description: "But hey, I have a description!", - rank: 0 + exp: 0, + tutorials: [] } ] as readonly Users[]; diff --git a/packages/api/src/modules/apiRouter.ts b/packages/api/src/modules/apiRouter.ts index 76d861e..ee7f2a4 100644 --- a/packages/api/src/modules/apiRouter.ts +++ b/packages/api/src/modules/apiRouter.ts @@ -3,6 +3,7 @@ import Router from "./Router"; import authRouter from "./auth/router"; import gamesRouter from "./games/router"; import textsRouter from "./texts/router"; +import tutorialsRouter from "./tutorials/router"; import usersRouter from "./users/router"; const apiRouter = new Router({ prefix: "/api" }); @@ -10,6 +11,7 @@ const apiRouter = new Router({ prefix: "/api" }); apiRouter.use(authRouter); apiRouter.use(gamesRouter); apiRouter.use(textsRouter); +apiRouter.use(tutorialsRouter); apiRouter.use(usersRouter); export default apiRouter.routes(); diff --git a/packages/api/src/modules/auth/router.test.ts b/packages/api/src/modules/auth/router.test.ts index f3de17c..996f824 100644 --- a/packages/api/src/modules/auth/router.test.ts +++ b/packages/api/src/modules/auth/router.test.ts @@ -12,7 +12,7 @@ const agent = request.agent(server); describe("Auth router", () => { before(async function() { - this.timeout(5000); + this.timeout(20000); await knex.migrate.latest(); await knex.seed.run(); }); diff --git a/packages/api/src/modules/texts/actions/addText.ts b/packages/api/src/modules/texts/actions/addText.ts index e1ae1cd..5b8a66f 100644 --- a/packages/api/src/modules/texts/actions/addText.ts +++ b/packages/api/src/modules/texts/actions/addText.ts @@ -1,6 +1,18 @@ import knex from "../../../../db/knex"; import Text from "../types/Text"; -export default async (text: string, ordered = true) => { - await knex("texts").insert({ text, ordered }); +export default async ( + text: string, + difficulty: number, + author: number, + ordered = true, + tutorial = false +) => { + await knex("texts").insert({ + text, + difficulty, + author, + ordered, + tutorial + }); }; diff --git a/packages/api/src/modules/texts/actions/getRandomText.ts b/packages/api/src/modules/texts/actions/getRandomText.ts index c7c9011..83ddbda 100644 --- a/packages/api/src/modules/texts/actions/getRandomText.ts +++ b/packages/api/src/modules/texts/actions/getRandomText.ts @@ -3,10 +3,13 @@ import knex from "../../../../db/knex"; export default async (typed = false, ordered = false) => { if (typed) { - const texts = await knex("texts").where({ ordered }); + const texts = await knex("texts").where({ + ordered, + tutorial: false + }); return texts[Math.floor(Math.random() * texts.length)]; } else { - const texts = await knex("texts"); + const texts = await knex("texts").where({ tutorial: false }); return texts[Math.floor(Math.random() * texts.length)]; } }; diff --git a/packages/api/src/modules/texts/router.test.ts b/packages/api/src/modules/texts/router.test.ts index e75f674..52fef06 100644 --- a/packages/api/src/modules/texts/router.test.ts +++ b/packages/api/src/modules/texts/router.test.ts @@ -7,15 +7,21 @@ const agent = request.agent(server); const newText = { text: "This is a text!", + difficulty: 1, + author: 1, + tutorial: false, ordered: true }; const newRandomText = { text: "this is a text", + difficulty: 1, + author: 1, + tutorial: false, ordered: false }; -describe("Users routes", async () => { +describe("Texts routes", async () => { // We don't need to rerun migrations or seeds because we did in the auth route it("Adds texts", async () => { diff --git a/packages/api/src/modules/texts/router.ts b/packages/api/src/modules/texts/router.ts index a7f3533..88a7a48 100644 --- a/packages/api/src/modules/texts/router.ts +++ b/packages/api/src/modules/texts/router.ts @@ -7,8 +7,9 @@ import getRandomText from "./actions/getRandomText"; const router = new Router({ prefix: "/texts" }); router.post("/addText", requireAdmin(), async (ctx, next) => { - const { text, ordered } = ctx.request.body; - await addText(text, ordered); + const { text, difficulty, ordered, tutorial } = ctx.request.body; + const { user } = ctx.session!; + await addText(text, difficulty, user, ordered, tutorial); ctx.status = 201; ctx.body = "Successfully added text"; await next(); diff --git a/packages/api/src/modules/texts/types/Text.ts b/packages/api/src/modules/texts/types/Text.ts index d20cd7d..375dab0 100644 --- a/packages/api/src/modules/texts/types/Text.ts +++ b/packages/api/src/modules/texts/types/Text.ts @@ -2,4 +2,7 @@ export default interface Text { id: number; text: string; ordered: boolean; + tutorial: boolean; + difficulty: number; + author: number; } diff --git a/packages/api/src/modules/tutorials/actions/completeTutorial.ts b/packages/api/src/modules/tutorials/actions/completeTutorial.ts new file mode 100644 index 0000000..68c6889 --- /dev/null +++ b/packages/api/src/modules/tutorials/actions/completeTutorial.ts @@ -0,0 +1,25 @@ +import { Tutorial } from "../types/Tutorial"; +import knex from "../../../../db/knex"; +import User from "../../users/types/User"; + +export default async (tutorialid: number, userid: number) => { + const tutorial = await knex("texts") + .where({ + id: tutorialid, + tutorial: true + }) + .first(); + console.log(tutorial); + const user = await knex("users") + .where({ + id: userid + }) + .first(); + if (!tutorial || !user) return false; + await knex("users") + .update({ + tutorials: knex.raw("array_append(tutorials, ?)", [tutorialid]) + }) + .where({ id: userid }); + return true; +}; diff --git a/packages/api/src/modules/tutorials/actions/getRandomTutorial.ts b/packages/api/src/modules/tutorials/actions/getRandomTutorial.ts new file mode 100644 index 0000000..ca4248f --- /dev/null +++ b/packages/api/src/modules/tutorials/actions/getRandomTutorial.ts @@ -0,0 +1,9 @@ +import Text from "../../texts/types/Text"; +import knex from "../../../../db/knex"; + +export default async () => { + const texts = await knex("texts").where({ + tutorial: true + }); + return texts[Math.floor(Math.random() * texts.length)]; +}; diff --git a/packages/api/src/modules/tutorials/actions/getTutorial.ts b/packages/api/src/modules/tutorials/actions/getTutorial.ts new file mode 100644 index 0000000..286937f --- /dev/null +++ b/packages/api/src/modules/tutorials/actions/getTutorial.ts @@ -0,0 +1,9 @@ +import { Tutorial } from "../types/Tutorial"; +import knex from "../../../../db/knex"; + +export default async (id: number) => { + const tutorial = await knex("texts") + .where({ id, tutorial: true }) + .first(); + return tutorial || null; +}; diff --git a/packages/api/src/modules/tutorials/router.test.ts b/packages/api/src/modules/tutorials/router.test.ts new file mode 100644 index 0000000..76957bf --- /dev/null +++ b/packages/api/src/modules/tutorials/router.test.ts @@ -0,0 +1,56 @@ +import request from "supertest"; +import { expect } from "chai"; + +import { server } from "../../index"; +import addText from "../texts/actions/addText"; +import User from "../users/types/User"; +import knex from "../../../db/knex"; + +const agent = request.agent(server); + +describe("Tutorials routes", async () => { + // We don't need to rerun migrations or seeds because we did in the auth route + + it("Completes a tutorial", async () => { + await agent.post("/api/auth/login").send({ + email: "Deliver@bullets.com", + password: "MaoGay" + }); + + await addText("blah", 1, 1, false, true); + const response = await agent + .post(`/api/tutorials/completeTutorial/`) + .send({ id: 3 }) + .set("Accept", "application/text") + .expect("Content-Type", /text/) + .expect(200); + + expect(response.text).to.deep.equal("Successfully completed tutorial"); + }); + + it("Cannot complete a non-existent tutorial", async () => { + const response = await agent + .post(`/api/tutorials/completeTutorial/`) + .send({ id: 1 }) + .set("Accept", "application/json") + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).to.deep.equal({ + status: 400, + message: "That tutorial does not exist!" + }); + }); + + it("Gets tutorials completed for the user", async () => { + const user = await knex("users") + .where({ + email: "Deliver@bullets.com" + }) + .first(); + + console.log(user); + + expect(user?.tutorials).to.deep.equal([3]); + }); +}); diff --git a/packages/api/src/modules/tutorials/router.ts b/packages/api/src/modules/tutorials/router.ts new file mode 100644 index 0000000..9da7d05 --- /dev/null +++ b/packages/api/src/modules/tutorials/router.ts @@ -0,0 +1,42 @@ +import Router from "../Router"; +import { requireAuthenticated } from "../auth/middleware/requireAuthenticated"; +import completeTutorial from "./actions/completeTutorial"; +import { HttpError } from "../../common/error/classes/httpError"; +import getTutorial from "./actions/getTutorial"; +import getRandomTutorial from "./actions/getRandomTutorial"; + +const router = new Router({ prefix: "/tutorials" }); + +router.post("/completeTutorial", requireAuthenticated(), async (ctx, next) => { + const { id } = ctx.request.body; + const { user } = ctx.session!; + const success = await completeTutorial(id, user); + if (!success) { + throw new HttpError(400, "That tutorial does not exist!"); + } + ctx.status = 200; + ctx.body = "Successfully completed tutorial"; + await next(); +}); + +router.get("/:id", async (ctx, next) => { + const { id } = ctx.params; + console.log(`Received ${id}`); + const tutorial = getTutorial(id); + if (!tutorial) { + throw new HttpError(400, "That tutorial does not exist!"); + } + console.log(tutorial); + ctx.status = 200; + ctx.body = tutorial; + await next(); +}); + +router.get("/random", async (ctx, next) => { + const tutorial = getRandomTutorial(); + ctx.status = 200; + ctx.body = tutorial; + await next(); +}); + +export default router.routes(); diff --git a/packages/api/src/modules/tutorials/types/Tutorial.ts b/packages/api/src/modules/tutorials/types/Tutorial.ts new file mode 100644 index 0000000..d96f52e --- /dev/null +++ b/packages/api/src/modules/tutorials/types/Tutorial.ts @@ -0,0 +1,3 @@ +import Text from "../../texts/types/Text"; + +export type Tutorial = Text; diff --git a/packages/api/src/modules/users/actions/createUser.ts b/packages/api/src/modules/users/actions/createUser.ts index 80a023c..d568a17 100644 --- a/packages/api/src/modules/users/actions/createUser.ts +++ b/packages/api/src/modules/users/actions/createUser.ts @@ -21,7 +21,8 @@ export const createUser = async ( ...user, role: "member", password: encryptedPassword, - rank: 0 + exp: 0, + tutorials: [] }, "*" ) diff --git a/packages/api/src/modules/users/types/User.ts b/packages/api/src/modules/users/types/User.ts index edc6d4b..6b295d9 100644 --- a/packages/api/src/modules/users/types/User.ts +++ b/packages/api/src/modules/users/types/User.ts @@ -3,7 +3,8 @@ export default interface User { email: string; name: string; password: string; - rank: number; + exp: number; role?: string | null; description?: string | null; + tutorials: number[]; }