Skip to content
115 changes: 90 additions & 25 deletions worklenz-backend/src/controllers/project-categories-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,64 @@ import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { getColor } from "../shared/utils";
import { WorklenzColorCodes } from "../shared/constants";
import { WorklenzColorShades } from "../shared/constants";
import { SqlHelper } from "../shared/sql-helpers";

export default class ProjectCategoriesController extends WorklenzControllerBase {

private static flatString(text: string) {
return (text || "").split(",").map(s => `'${s}'`).join(",");
}

@HandleExceptions()
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
public static async create(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `
INSERT INTO project_categories (name, team_id, created_by, color_code)
VALUES ($1, $2, $3, $4)
RETURNING id, name, color_code;
`;
const name = req.body.name.trim();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const result = await db.query(q, [name, req.user?.team_id, req.user?.id, name ? getColor(name) : null]);

// Validate and use provided color_code, or fall back to generated color
let colorCode: string | null = null;
if (req.body.color_code) {
// Validate color - accept both base colors and all shade variations
const validColors = [
...Object.keys(WorklenzColorShades),
...Object.values(WorklenzColorShades).flat(),
].map((c) => c.toLowerCase());

const providedColor = req.body.color_code.trim().toLowerCase();
if (validColors.includes(providedColor)) {
// Find the original case color from the valid colors
const allColors = [
...Object.keys(WorklenzColorShades),
...Object.values(WorklenzColorShades).flat(),
];
colorCode = allColors.find(c => c.toLowerCase() === providedColor) || providedColor;
} else {
// Invalid color provided, fall back to generated color
colorCode = name ? getColor(name) : null;
}
} else {
// No color provided, generate one
colorCode = name ? getColor(name) : null;
}

const result = await db.query(q, [
name,
req.user?.team_id,
req.user?.id,
colorCode,
]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}

@HandleExceptions()
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
public static async get(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `
SELECT id, name, color_code, (SELECT COUNT(*) FROM projects WHERE category_id = project_categories.id) AS usage
FROM project_categories
Expand All @@ -39,52 +74,82 @@ export default class ProjectCategoriesController extends WorklenzControllerBase
}

@HandleExceptions()
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
public static async getById(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `
SELECT id, name, color_code, (SELECT COUNT(*) FROM projects WHERE category_id = project_categories.id) AS usage
FROM project_categories
WHERE team_id = $1;`;
const result = await db.query(q, [req.params.id]);
return res.status(200).send(new ServerResponse(true, result.rows));
}

private static async getTeamsByOrg(teamId: string) {
const q = `SELECT id FROM teams WHERE in_organization(id, $1)`;
const result = await db.query(q, [teamId]);
return result.rows;
}

@HandleExceptions()
public static async getByMultipleTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {

public static async getByMultipleTeams(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const teams = await this.getTeamsByOrg(req.user?.team_id as string);
const teamIds = teams.map(team => team.id).join(",");
const teamIds = teams.map((team) => team.id);
const { clause, params } = SqlHelper.buildInClause(teamIds, 1);

const q = `SELECT id, name, color_code FROM project_categories WHERE team_id IN (${this.flatString(teamIds)});`;
const q = `SELECT id, name, color_code FROM project_categories WHERE team_id IN (${clause})`;

const result = await db.query(q);
const result = await db.query(q, params);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return res.status(200).send(new ServerResponse(true, result.rows));

}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@HandleExceptions()
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
public static async update(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
// Validate color - accept both base colors and all shade variations
const validColors = [
...Object.keys(WorklenzColorShades),
...Object.values(WorklenzColorShades).flat(),
].map((c) => c.toLowerCase());
if (!validColors.includes(req.body.color.toLowerCase())) {
return res.status(400).send(new ServerResponse(false, "Invalid color"));
}

// Validate name
const name =
typeof req.body.name === "string" ? req.body.name.trim() : undefined;
if (!name || name.length === 0) {
return res
.status(400)
.send(new ServerResponse(false, "Category name is required."));
}

const q = `
UPDATE project_categories
SET color_code = $2
SET name = $2, color_code = $3
WHERE id = $1
AND team_id = $3;
AND team_id = $4;
`;

if (!WorklenzColorCodes.includes(req.body.color))
return res.status(400).send(new ServerResponse(false, null));

const result = await db.query(q, [req.params.id, req.body.color, req.user?.team_id]);
const result = await db.query(q, [
req.params.id,
name,
req.body.color,
req.user?.team_id,
]);
Comment on lines +134 to +163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing null/type check on req.body.color — will throw on undefined.

Line 120 calls req.body.color.toLowerCase() without checking if req.body.color exists or is a string. If the client omits the color field, this throws a TypeError.

Also note the field name inconsistency: create() reads req.body.color_code (Line 28) while update() reads req.body.color (Line 120). This means the client must use different field names for create vs. update.

Suggested fix
+ if (!req.body.color || typeof req.body.color !== 'string') {
+   return res.status(400).send(new ServerResponse(false, "Color is required"));
+ }
+
  const validColors = [
    ...Object.keys(WorklenzColorShades),
    ...Object.values(WorklenzColorShades).flat(),
  ].map((c) => c.toLowerCase());
  if (!validColors.includes(req.body.color.toLowerCase())) {
    return res.status(400).send(new ServerResponse(false, "Invalid color"));
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate color - accept both base colors and all shade variations
const validColors = [
...Object.keys(WorklenzColorShades),
...Object.values(WorklenzColorShades).flat(),
].map((c) => c.toLowerCase());
if (!validColors.includes(req.body.color.toLowerCase())) {
return res.status(400).send(new ServerResponse(false, "Invalid color"));
}
// Validate name
const name =
typeof req.body.name === "string" ? req.body.name.trim() : undefined;
if (!name || name.length === 0) {
return res
.status(400)
.send(new ServerResponse(false, "Category name is required."));
}
const q = `
UPDATE project_categories
SET color_code = $2
SET name = $2, color_code = $3
WHERE id = $1
AND team_id = $3;
AND team_id = $4;
`;
if (!WorklenzColorCodes.includes(req.body.color))
return res.status(400).send(new ServerResponse(false, null));
const result = await db.query(q, [req.params.id, req.body.color, req.user?.team_id]);
const result = await db.query(q, [
req.params.id,
name,
req.body.color,
req.user?.team_id,
]);
// Validate color - accept both base colors and all shade variations
if (!req.body.color || typeof req.body.color !== 'string') {
return res.status(400).send(new ServerResponse(false, "Color is required"));
}
const validColors = [
...Object.keys(WorklenzColorShades),
...Object.values(WorklenzColorShades).flat(),
].map((c) => c.toLowerCase());
if (!validColors.includes(req.body.color.toLowerCase())) {
return res.status(400).send(new ServerResponse(false, "Invalid color"));
}
// Validate name
const name =
typeof req.body.name === "string" ? req.body.name.trim() : undefined;
if (!name || name.length === 0) {
return res
.status(400)
.send(new ServerResponse(false, "Category name is required."));
}
const q = `
UPDATE project_categories
SET name = $2, color_code = $3
WHERE id = $1
AND team_id = $4;
`;
const result = await db.query(q, [
req.params.id,
name,
req.body.color,
req.user?.team_id,
]);
🤖 Prompt for AI Agents
In `@worklenz-backend/src/controllers/project-categories-controller.ts` around
lines 115 - 144, The update handler currently calls req.body.color.toLowerCase()
without checking type and also uses a different field name than create; fix by
accepting the same field used in create (req.body.color_code) or normalizing
both names, first ensure color exists and is a string (e.g., const colorRaw =
typeof req.body.color === "string" ? req.body.color : req.body.color_code; if
(!colorRaw) return 400), then compute const color =
colorRaw.trim().toLowerCase() and use that when validating against
WorklenzColorShades and when binding the query parameters (replace
req.body.color with the normalized color or original color_code as appropriate);
update references in this function (WorklenzColorShades check and db.query
parameter array) to use the normalized variable.

return res.status(200).send(new ServerResponse(true, result.rows));
}

@HandleExceptions()
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
public static async deleteById(
req: IWorkLenzRequest,
res: IWorkLenzResponse
): Promise<IWorkLenzResponse> {
const q = `
DELETE
FROM project_categories
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import HandleExceptions from "../../decorators/handle-exceptions";
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
import { ServerResponse } from "../../models/server-response";
import { SqlHelper } from "../../shared/sql-helpers";
import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
import { getColor } from "../../shared/utils";
import PtTasksControllerBase, { GroupBy, ITaskGroup } from "./pt-tasks-controller-base";
Expand Down Expand Up @@ -33,12 +34,11 @@ export default class PtTasksController extends PtTasksControllerBase {
return PtTasksController.isCountsOnly(query) || query.parent_task;
}

private static flatString(text: string) {
return (text || "").split(" ").map(s => `'${s}'`).join(",");
}

private static getFilterByTemplatsWhereClosure(text: string) {
return text ? `template_id IN (${this.flatString(text)})` : "";
private static getFilterByTemplatsWhereClosure(text: string, paramOffset: number): { clause: string; params: string[] } {
if (!text) return { clause: "", params: [] };
const templateIds = text.split(" ").filter(id => id.trim());
const { clause } = SqlHelper.buildInClause(templateIds, paramOffset);
return { clause: `template_id IN (${clause})`, params: templateIds };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all usages of getFilterByTemplatsWhereClosure in the entire codebase
rg -n "getFilterByTemplatsWhereClosure" --type ts

Repository: Worklenz/worklenz

Length of output: 263


🏁 Script executed:

#!/bin/bash
# Read the file to check other usages of SqlHelper and file context
head -50 worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts | cat -n

Repository: Worklenz/worklenz

Length of output: 2476


🏁 Script executed:

#!/bin/bash
# Check total file length and search for all SqlHelper usages
wc -l worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts
echo "---"
rg -n "SqlHelper" worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts

Repository: Worklenz/worklenz

Length of output: 279


Remove unreferenced method and unused import.

The getFilterByTemplatsWhereClosure method (lines 37-42) is never called anywhere in the codebase and qualifies as dead code. Additionally, the SqlHelper import on line 8 is only used within this unreferenced method and should also be removed.

🤖 Prompt for AI Agents
In `@worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts`
around lines 37 - 42, Remove the dead helper and its unused import: delete the
private static method getFilterByTemplatsWhereClosure from the
pt-tasks-controller class and remove the corresponding SqlHelper import since
SqlHelper is only referenced by that method; ensure no other code references
getFilterByTemplatsWhereClosure and run tests/TS compile to confirm no remaining
usages.


private static getQuery(userId: string, options: ParsedQs) {
Expand Down
Loading
Loading