From a157cb3fede2a37c081e67feef87524eaee10e3d Mon Sep 17 00:00:00 2001 From: Akhil Reddy Date: Wed, 29 Jan 2025 15:44:24 +0530 Subject: [PATCH] Add task routes, bug fixes --- src/file/file.schema.ts | 27 +++++++ src/index.ts | 2 +- src/notes/notes.schema.ts | 19 +++++ src/routes.ts | 4 +- src/rts/rts.schema.ts | 28 +------- src/server.ts | 2 + src/task/task.controller.ts | 84 ++++++++++++++++++++++ src/task/task.route.ts | 82 +++++++++++++++++++++ src/task/task.schema.ts | 54 ++++++++++++++ src/task/task.service.ts | 140 ++++++++++++++++++++++++++++++++++++ 10 files changed, 413 insertions(+), 29 deletions(-) create mode 100644 src/notes/notes.schema.ts create mode 100644 src/task/task.controller.ts create mode 100644 src/task/task.route.ts create mode 100644 src/task/task.schema.ts create mode 100644 src/task/task.service.ts diff --git a/src/file/file.schema.ts b/src/file/file.schema.ts index 9b6385e..dc46034 100644 --- a/src/file/file.schema.ts +++ b/src/file/file.schema.ts @@ -31,6 +31,33 @@ const downloadFileResponse = z.object({ url: z.string().url(), }); +export const files = z + .object({ + pid: z.string().optional(), + name: z.string(), + type: z.enum(["folder", "file"]), + size: z.number().optional(), + files: z.array(z.lazy(() => files)).optional(), + }) + .superRefine((data, ctx) => { + const validateRecursive = (file: any) => { + if (file.type === "file" && !file.pid) { + ctx.addIssue({ + path: ["pid"], + message: 'pid is required when type is "file"', + code: z.ZodIssueCode.custom, + }); + } + if (file.files) { + file.files.forEach((nestedFile: any) => { + validateRecursive(nestedFile); + }); + } + }; + + validateRecursive(data); + }); + export const { schemas: fileSchemas, $ref: $file } = buildJsonSchemas( { uploadFileResponse, diff --git a/src/index.ts b/src/index.ts index 9a49859..445332e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import app from "./server"; (async () => { const PORT = parseInt(process.env.PORT ?? "8000"); - const DB_URI = process.env.DB_URI ?? ""; + const DB_URI = process.env.DB_TEST ?? ""; await mongoose.connect(DB_URI); await app.listen({ port: PORT, host: "0.0.0.0" }); diff --git a/src/notes/notes.schema.ts b/src/notes/notes.schema.ts new file mode 100644 index 0000000..dd4fec0 --- /dev/null +++ b/src/notes/notes.schema.ts @@ -0,0 +1,19 @@ +import mongoose from "mongoose"; + +const noteSchema = new mongoose.Schema({ + resourceId: { + type: mongoose.Types.ObjectId, + required: true, + }, + content: { + type: String, + required: true, + }, + createdAt: Date, + createdBy: { + type: mongoose.Types.ObjectId, + ref: "user", + }, +}); + +export const noteModel = mongoose.model("note", noteSchema); diff --git a/src/routes.ts b/src/routes.ts index ab87a47..3dce3f4 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -3,9 +3,10 @@ import userRoutes from "./user/user.route"; import organizationRoutes from "./organization/organization.route"; import { tokenRoutes } from "./tokens/token.route"; import { permitRoutes } from "./permit/permit.route"; -import { authHandler, hideFields } from "./auth"; +import { authHandler } from "./auth"; import { fileRoutes } from "./file/file.route"; import { rtsRoutes } from "./rts/rts.route"; +import { taskRoutes } from "./task/task.route"; export default async function routes(fastify: FastifyInstance) { fastify.addHook("preHandler", authHandler); @@ -15,4 +16,5 @@ export default async function routes(fastify: FastifyInstance) { fastify.register(permitRoutes, { prefix: "/permits" }); fastify.register(fileRoutes, { prefix: "/files" }); fastify.register(rtsRoutes, { prefix: "/rts" }); + fastify.register(taskRoutes, { prefix: "/tasks" }); } diff --git a/src/rts/rts.schema.ts b/src/rts/rts.schema.ts index 2c0c3ab..e201fa5 100644 --- a/src/rts/rts.schema.ts +++ b/src/rts/rts.schema.ts @@ -1,6 +1,7 @@ import { buildJsonSchemas } from "fastify-zod"; import mongoose from "mongoose"; import z from "zod"; +import { files } from "../file/file.schema"; import { pageQueryParams } from "../pagination"; const rtsSchema = new mongoose.Schema({ @@ -58,33 +59,6 @@ export const rtsFields = Object.keys(rtsSchema.paths).filter( export const rtsModel = mongoose.model("rts", rtsSchema, "rts"); -const files = z - .object({ - pid: z.string().optional(), - name: z.string(), - type: z.enum(["folder", "file"]), - size: z.number().optional(), - files: z.array(z.lazy(() => files)).optional(), - }) - .superRefine((data, ctx) => { - const validateRecursive = (file: any) => { - if (file.type === "file" && !file.pid) { - ctx.addIssue({ - path: ["pid"], - message: 'pid is required when type is "file"', - code: z.ZodIssueCode.custom, - }); - } - if (file.files) { - file.files.forEach((nestedFile: any) => { - validateRecursive(nestedFile); - }); - } - }; - - validateRecursive(data); - }); - const rtsCreateInput = z.object({ county: z.string(), client: z.string().optional(), diff --git a/src/server.ts b/src/server.ts index fbfbb3a..91aab83 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,6 +13,7 @@ import { fileSchemas } from "./file/file.schema"; import { oauth } from "./oauth"; import { authRoutes } from "./auth/auth.route"; import { rtsSchemas } from "./rts/rts.schema"; +import { taskSchemas } from "./task/task.schema"; const app = fastify({ logger: true }); @@ -39,6 +40,7 @@ for (const schema of [ ...permitSchemas, ...fileSchemas, ...rtsSchemas, + ...taskSchemas, ]) { app.addSchema(schema); } diff --git a/src/task/task.controller.ts b/src/task/task.controller.ts new file mode 100644 index 0000000..7fe4253 --- /dev/null +++ b/src/task/task.controller.ts @@ -0,0 +1,84 @@ +import { FastifyReply, FastifyRequest } from "fastify"; +import { CreateTaskInput, UpdateTaskInput } from "./task.schema"; +import { + createTask, + deleteTask, + getTask, + listTasks, + updateTask, +} from "./task.service"; +import { PageQueryParams } from "../pagination"; + +export async function createTaskHandler( + req: FastifyRequest, + res: FastifyReply +) { + const input = req.body as CreateTaskInput; + + try { + const rts = await createTask(input, req.user); + return res.code(201).send(rts); + } catch (err) { + return err; + } +} + +export async function getTaskHandler(req: FastifyRequest, res: FastifyReply) { + const { taskId } = req.params as { taskId: string }; + + try { + const task = await getTask(taskId, req.user.tenantId); + if (task == null) + return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(task); + } catch (err) { + return err; + } +} + +export async function listTaskHandler(req: FastifyRequest, res: FastifyReply) { + const queryParams = req.query as PageQueryParams; + + try { + const taskList = await listTasks(queryParams, req.user.tenantId); + return res.code(200).send(taskList); + } catch (err) { + return err; + } +} + +export async function updateTaskHandler( + req: FastifyRequest, + res: FastifyReply +) { + const input = req.body as UpdateTaskInput; + const { taskId } = req.params as { taskId: string }; + + try { + const updatedTask = await updateTask(taskId, input, req.user.tenantId); + if (!updatedTask) + return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(updatedTask); + } catch (err) { + return err; + } +} + +export async function deleteTaskHandler( + req: FastifyRequest, + res: FastifyReply +) { + const { taskId } = req.params as { taskId: string }; + + try { + const deleteResult = await deleteTask(taskId, req.user.tenantId); + if (deleteResult.deletedCount == 0) + return res.code(404).send({ error: "resource not found" }); + + return res.code(204).send(); + } catch (err) { + return err; + } +} diff --git a/src/task/task.route.ts b/src/task/task.route.ts new file mode 100644 index 0000000..64a9d98 --- /dev/null +++ b/src/task/task.route.ts @@ -0,0 +1,82 @@ +import { FastifyInstance } from "fastify"; +import { $task } from "./task.schema"; +import { + createTaskHandler, + deleteTaskHandler, + getTaskHandler, + listTaskHandler, + updateTaskHandler, +} from "./task.controller"; + +export async function taskRoutes(fastify: FastifyInstance) { + fastify.post( + "/", + { + schema: { + body: $task("createTaskInput"), + }, + config: { requiredClaims: ["task:write"] }, + preHandler: [fastify.authorize], + }, + createTaskHandler + ); + + fastify.get( + "/:taskId", + { + schema: { + params: { + type: "object", + properties: { + taskId: { type: "string" }, + }, + }, + }, + config: { requiredClaims: ["task:read"] }, + preHandler: [fastify.authorize], + }, + getTaskHandler + ); + + fastify.get( + "/", + { + schema: { + querystring: $task("pageQueryParams"), + }, + + config: { requiredClaims: ["task:read"] }, + preHandler: [fastify.authorize], + }, + listTaskHandler + ); + + fastify.patch( + "/:taskId", + { + schema: { + params: { + type: "object", + properties: { taskId: { type: "string" } }, + }, + body: $task("updateTaskInput"), + }, + }, + updateTaskHandler + ); + + fastify.delete( + "/:taskId", + { + schema: { + params: { + type: "object", + properties: { taskId: { type: "string" } }, + }, + }, + config: { requiredClaims: ["task:delete"] }, + preHandler: [fastify.authorize], + }, + deleteTaskHandler + ); +} diff --git a/src/task/task.schema.ts b/src/task/task.schema.ts new file mode 100644 index 0000000..7023112 --- /dev/null +++ b/src/task/task.schema.ts @@ -0,0 +1,54 @@ +import mongoose from "mongoose"; +import { z } from "zod"; +import { files } from "../file/file.schema"; +import { buildJsonSchemas } from "fastify-zod"; +import { pageQueryParams } from "../pagination"; + +const taskSchema = new mongoose.Schema({ + tenantId: { type: String, required: true }, + pid: { type: String, required: true, unique: true }, + title: String, + dueDate: Date, + files: Object, + createdAt: Date, + createdBy: { + type: mongoose.Types.ObjectId, + ref: "user", + }, + assignedTo: { + type: mongoose.Types.ObjectId, + ref: "user", + }, +}); + +export const taskFields = Object.keys(taskSchema.paths).filter( + (path) => path !== "__v" +); + +export const taskModel = mongoose.model("task", taskSchema); + +const createTaskInput = z.object({ + title: z.string(), + dueDate: z.date().optional(), + files: z.array(files).optional(), + assignedTo: z.string().optional(), +}); + +const updateTaskInput = z.object({ + title: z.string().optional(), + dueDate: z.date().optional(), + files: z.array(files).optional(), + assignedTo: z.string().optional(), +}); + +export type CreateTaskInput = z.infer; +export type UpdateTaskInput = z.infer; + +export const { schemas: taskSchemas, $ref: $task } = buildJsonSchemas( + { + createTaskInput, + updateTaskInput, + pageQueryParams, + }, + { $id: "task" } +); diff --git a/src/task/task.service.ts b/src/task/task.service.ts new file mode 100644 index 0000000..1dcaaee --- /dev/null +++ b/src/task/task.service.ts @@ -0,0 +1,140 @@ +import { AuthenticatedUser } from "../auth"; +import { getFilterObject, getSortObject, PageQueryParams } from "../pagination"; +import { generateId } from "../utils/id"; +import { + CreateTaskInput, + taskFields, + taskModel, + UpdateTaskInput, +} from "./task.schema"; + +export async function createTask( + input: CreateTaskInput, + user: AuthenticatedUser +) { + if (!input.files) { + return await taskModel.create({ + ...input, + tenantId: user.tenantId, + pid: generateId(), + createdAt: new Date(), + createdBy: user.userId ?? null, + }); + } else { + return await taskModel.create({ + tenantId: user.tenantId, + pid: generateId(), + documents: [ + { + files: input.files, + createdAt: new Date(), + createdBy: user.userId ?? null, + }, + ], + createdAt: new Date(), + createdBy: user.userId ?? null, + }); + } +} + +export async function updateTask( + taskId: string, + input: UpdateTaskInput, + tenantId: string +) { + const updatedTask = await taskModel + .findOneAndUpdate({ tenantId: tenantId, pid: taskId }, input, { new: true }) + .populate({ path: "createdBy", select: "pid name avatar" }) + .populate({ path: "assignedTo", select: "pid name avatar" }); + + return updatedTask; +} + +export async function getTask(taskId: string, tenantId: string) { + return await taskModel + .findOne({ tenantId: tenantId, pid: taskId }) + .populate({ path: "createdBy", select: "pid name avatar" }) + .populate({ path: "assignedTo", select: "pid name avatar" }); +} + +export async function listTasks(params: PageQueryParams, tenantId: string) { + const page = params.page || 1; + const pageSize = params.pageSize || 10; + const sortObj = getSortObject(params, taskFields); + const filterObj = getFilterObject(params, taskFields); + + const taskList = await taskModel.aggregate([ + { + $match: { $and: [{ tenantId: tenantId }, ...filterObj] }, + }, + { + $lookup: { + from: "users", + localField: "createdBy", + foreignField: "_id", + as: "createdBy", + }, + }, + { + $lookup: { + from: "users", + localField: "assignedTo", + foreignField: "_id", + as: "assignedTo", + }, + }, + { + $project: { + _id: 1, + pid: 1, + createdAt: 1, + createdBy: { + $let: { + vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } }, + in: { + _id: "$$createdBy._id", + pid: "$$createdBy.pid", + name: "$$createdBy.name", + }, + }, + }, + client: { + $let: { + vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } }, + in: { + _id: "$$assignedTo._id", + pid: "$$assignedTo.pid", + name: "$$assignedTo.name", + }, + }, + }, + }, + }, + { + $facet: { + metadata: [{ $count: "count" }], + data: [ + { $skip: (page - 1) * pageSize }, + { $limit: pageSize }, + { $sort: sortObj }, + ], + }, + }, + ]); + + if (taskList[0].data.length === 0) + return { tasks: [], metadata: { count: 0, page, pageSize } }; + + return { + tasks: taskList[0]?.data, + metadata: { + count: taskList[0].metadata[0].count, + page, + pageSize, + }, + }; +} + +export async function deleteTask(taskId: string, tenantId: string) { + return await taskModel.deleteOne({ pid: taskId, tenantId: tenantId }); +}