add ctasks routes

This commit is contained in:
2025-06-07 14:59:22 +05:30
parent 01b40dd757
commit bec293193f
9 changed files with 600 additions and 1 deletions

View File

@@ -0,0 +1,99 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { CreateTaskInput, UpdateTaskInput } from "./ctask.schema";
import {
createTask,
deleteTask,
getTask,
listTasks,
searchTasks,
updateTask,
} from "./ctask.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);
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);
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);
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);
if (deleteResult.deletedCount == 0)
return res.code(404).send({ error: "resource not found" });
return res.code(204).send();
} catch (err) {
return err;
}
}
export async function searchTaskHandler(
req: FastifyRequest,
res: FastifyReply
) {
const queryParams = req.query as PageQueryParams;
try {
const taskList = await searchTasks(queryParams, req.user);
return res.code(200).send(taskList);
} catch (err) {
return err;
}
}

127
src/ctask/ctask.route.ts Normal file
View File

@@ -0,0 +1,127 @@
import { FastifyInstance } from "fastify";
import { $ctask } from "./ctask.schema";
import {
createTaskHandler,
deleteTaskHandler,
getTaskHandler,
listTaskHandler,
searchTaskHandler,
updateTaskHandler,
} from "./ctask.controller";
import { noteRoutes } from "../note/note.route";
import { getUniqueFields } from "../unique";
export async function ctaskRoutes(fastify: FastifyInstance) {
fastify.post(
"/",
{
schema: {
body: $ctask("createTaskInput"),
},
config: { requiredClaims: ["ctask:write"] },
preHandler: [fastify.authorize],
},
createTaskHandler
);
fastify.get(
"/:taskId",
{
schema: {
params: {
type: "object",
properties: {
taskId: { type: "string" },
},
},
},
config: { requiredClaims: ["ctask:read"] },
preHandler: [fastify.authorize],
},
getTaskHandler
);
fastify.get(
"/",
{
schema: {
querystring: $ctask("pageQueryParams"),
},
config: { requiredClaims: ["ctask:read"] },
preHandler: [fastify.authorize],
},
listTaskHandler
);
fastify.patch(
"/:taskId",
{
schema: {
params: {
type: "object",
properties: { taskId: { type: "string" } },
},
body: $ctask("updateTaskInput"),
},
config: { requiredClaims: ["ctask:write"] },
preHandler: [fastify.authorize],
},
updateTaskHandler
);
fastify.delete(
"/:taskId",
{
schema: {
params: {
type: "object",
properties: { taskId: { type: "string" } },
},
},
config: { requiredClaims: ["ctask:delete"] },
preHandler: [fastify.authorize],
},
deleteTaskHandler
);
fastify.get(
"/search",
{
schema: {
querystring: $ctask("pageQueryParams"),
},
config: { requiredClaims: ["ctask:read"] },
preHandler: [fastify.authorize],
},
searchTaskHandler
);
fastify.get(
"/fields/:field",
{
schema: {
params: {
type: "object",
properties: {
field: { type: "string" },
},
},
},
config: { requiredClaims: ["ctask:read"] },
preHandler: [fastify.authorize],
},
async (req, res) => {
const { field } = req.params as { field: string };
try {
const uniqueValues = await getUniqueFields(field, "task", req.user);
return res.code(200).send(uniqueValues);
} catch (err) {
return err;
}
}
);
await noteRoutes(fastify);
}

99
src/ctask/ctask.schema.ts Normal file
View File

@@ -0,0 +1,99 @@
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 },
orgId: { type: String, required: true, ref: "organization" },
pid: { type: String, required: true, unique: true },
title: String,
dueDate: Date,
labels: [String],
priority: String,
stage: new mongoose.Schema(
{
pipeline: Array,
currentStage: Number,
},
{ _id: false }
),
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("ctask", taskSchema, "ctask");
const createTaskInput = z.object({
title: z.string(),
dueDate: z.date().optional(),
files: z.array(files).optional(),
assignedTo: z.string().optional(),
labels: z.array(z.string()).optional(),
priority: z.string().optional(),
stage: z
.object({
pipeline: z.array(
z.object({
name: z.string(),
date: z.date().nullable().optional(),
description: z.string().optional(),
comment: z.string().optional(),
})
),
currentStage: z.number(),
})
.optional(),
});
const updateTaskInput = z.object({
title: z.string().optional(),
dueDate: z.date().optional(),
files: z.array(files).optional(),
assignedTo: z.string().optional(),
labels: z.array(z.string()).optional(),
priority: z.string().optional(),
stage: z
.object({
pipeline: z.array(
z.object({
name: z.string(),
date: z.date().nullable().optional(),
description: z.string().optional(),
comment: z.string().optional(),
})
),
currentStage: z.number(),
})
.optional(),
});
const uploadTaskInput = z.object({
files: z.array(files),
});
export type CreateTaskInput = z.infer<typeof createTaskInput>;
export type UpdateTaskInput = z.infer<typeof updateTaskInput>;
export type UploadTaskInput = z.infer<typeof uploadTaskInput>;
export const { schemas: cTaskSchemas, $ref: $ctask } = buildJsonSchemas(
{
createTaskInput,
updateTaskInput,
uploadTaskInput,
pageQueryParams,
},
{ $id: "ctask" }
);

262
src/ctask/ctask.service.ts Normal file
View File

@@ -0,0 +1,262 @@
import { AuthenticatedUser } from "../auth";
import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
import { generateId } from "../utils/id";
import { taskPipeline } from "../utils/pipeline";
import {
CreateTaskInput,
taskFields,
taskModel,
UpdateTaskInput,
UploadTaskInput,
} from "./ctask.schema";
export async function createTask(
input: CreateTaskInput,
user: AuthenticatedUser
) {
if (!input.stage) {
input.stage = {
pipeline: taskPipeline,
currentStage: 0,
};
}
const task = await taskModel.create({
...input,
tenantId: user.tenantId,
orgId: user.orgId,
pid: generateId(),
createdAt: new Date(),
createdBy: user.userId ?? null,
});
return await taskModel
.findOne({ pid: task.pid })
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" });
}
export async function updateTask(
taskId: string,
input: UpdateTaskInput,
user: AuthenticatedUser
) {
const updatedTask = await taskModel
.findOneAndUpdate(
{ tenantId: user.tenantId, orgId: user.orgId, 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, user: AuthenticatedUser) {
return await taskModel
.findOne({ tenantId: user.tenantId, orgId: user.orgId, pid: taskId })
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" });
}
export async function listTasks(
params: PageQueryParams,
user: AuthenticatedUser
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const sortObj = getSortObject(params, taskFields);
const filterObj = getFilterObject(params) || [];
const taskList = await taskModel.aggregate([
{
$match: {
$and: [
{ tenantId: user.tenantId },
{ orgId: user.orgId },
...filterObj,
],
},
},
{
$lookup: {
from: "users",
localField: "createdBy",
foreignField: "_id",
as: "createdBy",
},
},
{
$lookup: {
from: "users",
localField: "assignedTo",
foreignField: "_id",
as: "assignedTo",
},
},
{
$project: {
_id: 1,
pid: 1,
title: 1,
dueDate: 1,
labels: 1,
priority: 1,
documents: 1,
stage: 1,
createdAt: 1,
createdBy: {
$let: {
vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } },
in: {
_id: "$$createdBy._id",
pid: "$$createdBy.pid",
name: "$$createdBy.name",
},
},
},
assignedTo: {
$let: {
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } },
in: {
_id: "$$assignedTo._id",
pid: "$$assignedTo.pid",
name: "$$assignedTo.name",
},
},
},
},
},
{
$facet: {
metadata: [{ $count: "count" }],
data: [
{ $sort: sortObj },
{ $skip: (page - 1) * pageSize },
{ $limit: pageSize },
],
},
},
]);
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, user: AuthenticatedUser) {
return await taskModel.deleteOne({
pid: taskId,
orgId: user.orgId,
tenantId: user.tenantId,
});
}
export async function searchTasks(
params: PageQueryParams,
user: AuthenticatedUser
) {
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const sortObj = getSortObject(params, taskFields);
const filterObj = getFilterObject(params) || [];
const regex = new RegExp(params.searchToken, "i");
const taskList = await taskModel.aggregate([
{
$match: {
$and: [
{ tenantId: user.tenantId },
{ orgId: user.orgId },
...filterObj,
],
},
},
{
$match: {
$or: [{ title: { $regex: regex } }],
},
},
{
$lookup: {
from: "users",
localField: "createdBy",
foreignField: "_id",
as: "createdBy",
},
},
{
$lookup: {
from: "users",
localField: "assignedTo",
foreignField: "_id",
as: "assignedTo",
},
},
{
$project: {
_id: 1,
pid: 1,
title: 1,
dueDate: 1,
labels: 1,
priority: 1,
documents: 1,
stage: 1,
createdAt: 1,
createdBy: {
$let: {
vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } },
in: {
_id: "$$createdBy._id",
pid: "$$createdBy.pid",
name: "$$createdBy.name",
},
},
},
assignedTo: {
$let: {
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } },
in: {
_id: "$$assignedTo._id",
pid: "$$assignedTo.pid",
name: "$$assignedTo.name",
},
},
},
},
},
{
$facet: {
metadata: [{ $count: "count" }],
data: [
{ $sort: sortObj },
{ $skip: (page - 1) * pageSize },
{ $limit: pageSize },
],
},
},
]);
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,
},
};
}

View File

@@ -13,6 +13,7 @@ import { configRoutes } from "./config/config.route";
import { mailProxyRoutes } from "./mailProxy/mailProxy.route";
import { viewRoutes } from "./view/view.route";
import { processedRoutes } from "./processed/processed.route";
import { ctaskRoutes } from "./ctask/ctask.route";
export default async function routes(fastify: FastifyInstance) {
fastify.addHook("preHandler", authHandler);
@@ -23,6 +24,7 @@ export default async function routes(fastify: FastifyInstance) {
fastify.register(fileRoutes, { prefix: "/files" });
fastify.register(rtsRoutes, { prefix: "/rts" });
fastify.register(taskRoutes, { prefix: "/tasks" });
fastify.register(ctaskRoutes, { prefix: "/ctasks" });
fastify.register(notificationRoutes, { prefix: "/notifications" });
fastify.register(mailProxyRoutes, { prefix: "/proxy" });
fastify.register(configRoutes, { prefix: "/config" });

View File

@@ -21,6 +21,7 @@ import { configSchemas } from "./config/config.schema";
import { mailSchemas } from "./mailProxy/mailProxy.schema";
import { viewSchemas } from "./view/view.schema";
import { processedSchemas } from "./processed/processed.schema";
import { cTaskSchemas } from "./ctask/ctask.schema";
const app = fastify({ logger: true, trustProxy: true });
@@ -49,6 +50,7 @@ for (const schema of [
...fileSchemas,
...rtsSchemas,
...taskSchemas,
...cTaskSchemas,
...notificationSchemas,
...noteSchemas,
...configSchemas,

View File

@@ -65,6 +65,8 @@ export async function taskRoutes(fastify: FastifyInstance) {
},
body: $task("updateTaskInput"),
},
config: { requiredClaims: ["task:write"] },
preHandler: [fastify.authorize],
},
updateTaskHandler
);
@@ -119,7 +121,7 @@ export async function taskRoutes(fastify: FastifyInstance) {
},
},
},
config: { requiredClaims: ["rts:read"] },
config: { requiredClaims: ["task:read"] },
preHandler: [fastify.authorize],
},
async (req, res) => {

View File

@@ -20,6 +20,9 @@ export type Claim =
| "task:read"
| "task:write"
| "task:delete"
| "ctask:read"
| "ctask:write"
| "ctask:delete"
| "notification:read"
| "notification:write"
| "notification:delete"

View File

@@ -126,6 +126,9 @@ export const rules: Record<
"view:read",
"view:write",
"view:delete",
"ctask:read",
"ctask:write",
"ctask:delete",
"note:read",
"note:write",
],