Skip to content

Commit

Permalink
implemented update task
Browse files Browse the repository at this point in the history
  • Loading branch information
vuvincent committed Sep 13, 2023
1 parent 39a6c19 commit f6d54ac
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `originalBuildText` to the `Task` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `Task` ADD COLUMN `originalBuildText` VARCHAR(191) NOT NULL;
2 changes: 2 additions & 0 deletions apps/api-v2/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

model User {
id String @id @default(uuid())
email String @unique
Expand All @@ -17,6 +18,7 @@ model User {

model Task {
id String @id @default(uuid())
originalBuildText String
title String
description String?
done Boolean @default(false)
Expand Down
7 changes: 2 additions & 5 deletions apps/api-v2/src/mws.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import express, { Request, Response, NextFunction } from "express";
import {
IAuthCtx,
SupernovaRequestValidationSchema,
SupernovaResponse,
} from "./types";
import { IAuthCtx, SupernovaResponse } from "./types";
import jwt from "jsonwebtoken";
import config from "./config";
import { redis } from "./db";
import { AnyZodObject, z } from "zod";
import { SupernovaRequestValidationSchema } from "@supernova/types";

// TODO: redirect to the web app on errors instead of sending a JSON response
export const authenticateJWTMiddleware = async (
Expand Down
58 changes: 57 additions & 1 deletion apps/api-v2/src/routers/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { authenticateJWTMiddleware, validateRequestSchema } from "../mws";
import { getAuthContext } from "../utils";
import { prisma } from "../db";
import { SupernovaResponse } from "../types";
import { createTaskRequestSchema } from "@supernova/types";
import {
createTaskRequestSchema,
updateTaskRequestSchema,
} from "@supernova/types";

export const buildTasksRouter = () => {
const router = Router();
Expand All @@ -12,7 +15,16 @@ export const buildTasksRouter = () => {
try {
const authCtx = getAuthContext(req);
// get all tasks for the user
// sort by earliest one first; ones with no start time last
const tasks = await prisma.task.findMany({
orderBy: [
{
startAt: {
sort: "asc",
nulls: "last",
},
},
],
where: {
userId: authCtx.sub,
},
Expand Down Expand Up @@ -45,6 +57,7 @@ export const buildTasksRouter = () => {
const task = await prisma.task.create({
data: {
title: req.body.title,
originalBuildText: req.body.originalBuildText,
description: req.body.description,
startAt: req.body.startAt,
expectedDurationSeconds: req.body.expectedDurationSeconds,
Expand All @@ -70,5 +83,48 @@ export const buildTasksRouter = () => {
}
}
);

// update task
// can also use this for marking task as done
router.put(
"/tasks/:id",
authenticateJWTMiddleware,
validateRequestSchema(updateTaskRequestSchema),
async (req, res) => {
try {
const authCtx = getAuthContext(req);
const task = await prisma.task.update({
where: {
id: req.params.id,
userId: authCtx.sub,
},
data: {
title: req.body.title,
originalBuildText: req.body.originalBuildText,
description: req.body.description,
startAt: req.body.startAt,
expectedDurationSeconds: req.body.expectedDurationSeconds,
},
});
return res.status(200).json(
new SupernovaResponse({
message: "Task updated successfully",
data: task,
})
);
} catch (e) {
if (e instanceof Error) {
console.error(e);
return res.status(500).json(
new SupernovaResponse({
message: e.message,
error: "Internal Server Error",
})
);
}
}
}
);

return router;
};
7 changes: 0 additions & 7 deletions apps/api-v2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,3 @@ export type EncodedProfileTokenClaims = { user: Profile } & jwt.JwtPayload;
// sub is the user ID which will be string since the @authenticateJWTMiddleware middleware
// will verify that the token is valid and has a sub claim
export type IAuthCtx = jwt.JwtPayload & { sub: string };

// any validation schema
export type SupernovaRequestValidationSchema = z.ZodObject<{
body?: AnyZodObject;
query?: AnyZodObject;
params?: AnyZodObject;
}>;
37 changes: 26 additions & 11 deletions apps/desktop-v2/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,6 @@ function Home() {
const handleCreateOrUpdateTask = useCallback(
async (task: ISupernovaTask) => {
if (chosenTaskIndex !== -1) {
setTasks(
tasks.map((t, i) => {
if (i === chosenTaskIndex) {
return task;
} else {
return t;
}
})
);
// // update task in backend
// if (db === null) {
// console.error("Database not initialized");
Expand All @@ -128,14 +119,37 @@ function Home() {
try {
console.log("updating task in backend...");
// await LocalDB.updateTask(db, task);
const oldTask = tasks[chosenTaskIndex];
// if a field is defined on oldTask but undefined on task, it means we're deleting it i.e setting to null
const res = await supernovaAPI.updateTask({
body: {
title: task.title,
originalBuildText: task.originalBuildText,
description: task.description,
startAt:
oldTask.startTime !== undefined && task.startTime === undefined
? null
: task.startTime?.toISOString(),
expectedDurationSeconds:
oldTask.expectedDurationSeconds !== undefined &&
task.expectedDurationSeconds === undefined
? null
: task.expectedDurationSeconds,
},
params: {
id: task.id,
},
});
if (res.type === "error") {
throw new Error(res.message);
}
console.log("updated successfully");
makeToast("Task updated successfully", "success");
setRefetchTasks(true); // refetch the tasks
} catch (e: any) {
console.error(e);
}
} else {
setTasks([...tasks, task]);
// create task in backend
// if (db === null) {
// console.error("Database not initialized");
Expand All @@ -147,6 +161,7 @@ function Home() {
await supernovaAPI.addTask({
body: {
title: task.title,
originalBuildText: task.originalBuildText,
description: task.description,
startAt: task.startTime?.toISOString(),
expectedDurationSeconds: task.expectedDurationSeconds,
Expand Down Expand Up @@ -392,7 +407,7 @@ function Home() {
<div className="text-slate-400 text-[16px]">Loading...</div>
</div>
) : taskFetchState.status === "success" ? (
<div className="flex flex-col items-center w-full max-h-full gap-2 overflow-clip">
<div className="flex flex-col items-center w-full max-h-full gap-2 overflow-clip max-w-3xl">
<hr className="w-64" />
{tasks.length === 0 && (
<div className="w-64">
Expand Down
2 changes: 1 addition & 1 deletion packages/api-client/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const nullToUndefined = (v: any) => (v === null ? undefined : v);
export const supernovaTaskConverter: Converter<any, ISupernovaTask> = {
convert: (t) => ({
id: t.id,
originalBuildText: t.title, // TODO: will do this when implementing build text
originalBuildText: t.originalBuildText,
title: t.title,
description: nullToUndefined(t.description),
expectedDurationSeconds: nullToUndefined(t.expectedDurationSeconds),
Expand Down
14 changes: 14 additions & 0 deletions packages/api-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CreateTaskRequest,
ISupernovaTask,
SupernovaResponse,
UpdateTaskRequest,
} from "@supernova/types";
import {
supernovaResponseConverter,
Expand Down Expand Up @@ -50,6 +51,19 @@ export default class SupernovaAPI {
}).then(supernovaResponseConverter.convert);
}

public updateTask(
request: UpdateTaskRequest
): Promise<SupernovaResponse<ISupernovaTask>> {
return fetch(`${this.baseUrl}/tasks/${request.params.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(request.body),
}).then(supernovaResponseConverter.convert);
}

public authenticate(): Promise<SupernovaResponse> {
return fetch(`${this.baseUrl}/auth`, {
headers: {
Expand Down
25 changes: 24 additions & 1 deletion packages/types/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { z } from "zod";
import { AnyZodObject, z } from "zod";

// any validation schema
export type SupernovaRequestValidationSchema = z.ZodObject<{
body?: AnyZodObject;
query?: AnyZodObject;
params?: AnyZodObject;
}>;

const dateString = () =>
z.string().refine((value) => {
Expand All @@ -8,9 +15,25 @@ const dateString = () =>
export const createTaskRequestSchema = z.object({
body: z.object({
title: z.string(),
originalBuildText: z.string(),
description: z.string().optional(),
startAt: dateString().optional(),
expectedDurationSeconds: z.number().positive().int().optional(),
}),
});
export type CreateTaskRequest = z.infer<typeof createTaskRequestSchema>;

// if it's null then on the backend it will be cleared on the backend
export const updateTaskRequestSchema = z.object({
body: z.object({
title: z.string().optional(), // this is not nullable because a task must have a title
originalBuildText: z.string().nullable().optional(),
description: z.string().nullable().optional(),
startAt: dateString().nullable().optional(),
expectedDurationSeconds: z.number().positive().int().nullable().optional(),
}),
params: z.object({
id: z.string(),
}),
});
export type UpdateTaskRequest = z.infer<typeof updateTaskRequestSchema>;

0 comments on commit f6d54ac

Please sign in to comment.