Skip to content

Yb sep 22 statuses to organizational backend#2193

Merged
MuhammadKhalilzadeh merged 8 commits intodevelopfrom
yb-sep-22-statuses-to-organizational-backend
Sep 29, 2025
Merged

Yb sep 22 statuses to organizational backend#2193
MuhammadKhalilzadeh merged 8 commits intodevelopfrom
yb-sep-22-statuses-to-organizational-backend

Conversation

@Aryanak47
Copy link
Copy Markdown
Contributor

Describe your changes

This PR implements a backend for project status tracking system for organizational frameworks. Projects can now have one of seven status values that can be managed through a new API endpoint and will be displayed in a Settings tab within the framework UI.

Write your issue number after "Fixes "

Fixes #2157

Please ensure all items are checked off before requesting a review:

  • I deployed the code locally.
  • I have performed a self-review of my code.
  • I have included the issue # in the PR.
  • I have labelled the PR correctly.
  • The issue I am working on is assigned to me.
  • I have avoided using hardcoded values to ensure scalability and maintain consistency across the application.
  • I have ensured that font sizes, color choices, and other UI elements are referenced from the theme.
  • My pull request is focused and addresses a single, specific feature.
  • If there are UI changes, I have attached a screenshot or video to this PR.
image

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 22, 2025

Walkthrough

Adds project status support across backend: new ProjectStatus enum, updated project interface and model column, migration adding public and per-tenant status columns, utility to allow status updates, a new PATCH /:id/status route and controller action that validates and updates status within a transaction (controller implementation duplicated in diff).

Changes

Cohort / File(s) Summary
Controller: status update API
Servers/controllers/project.ctrl.ts
Imports ProjectStatus and adds exported updateProjectStatus(req, res) that validates status, starts a transaction, checks project existence, updates status, last_updated, last_updated_by via updateProjectByIdQuery, commits/rolls back, and logs. (Function appears twice in diff — duplicate implementation.)
Database migration: project status
Servers/database/migrations/20250922000001-add-project-status.js
Adds a public projects.status ENUM with seven values and per-tenant projects.status columns (backfilled then set NOT NULL) inside a transaction; down migration removes them.
Domain: enum and typing
Servers/domain.layer/enums/project-status.enum.ts, Servers/domain.layer/interfaces/i.project.ts
Adds ProjectStatus enum (seven string states) and adds optional status?: ProjectStatus to IProjectAttributes.
Model: schema mapping
Servers/domain.layer/models/project/project.model.ts
Adds status: ProjectStatus column on ProjectModel as an ENUM column, non-nullable, default NOT_STARTED.
Routing: new endpoint
Servers/routes/project.route.ts
Registers PATCH /:id/status and maps it to updateProjectStatus.
Utilities & tenant creation script
Servers/utils/project.utils.ts, Servers/scripts/createNewTenant.ts
updateProjectByIdQuery now includes status in updatable fields; tenant creation script adds status column with default 'Not started' when creating tenant schema.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client as C
  participant Router as R
  participant Controller as CTRL
  participant DB as DB

  C->>R: PATCH /projects/:id/status { status }
  R->>CTRL: updateProjectStatus(req, res)
  CTRL->>DB: BEGIN TRANSACTION
  CTRL->>CTRL: Validate status ∈ ProjectStatus
  alt Invalid status
    CTRL-->>C: 400 Bad Request
    CTRL->>DB: ROLLBACK
  else Valid status
    CTRL->>DB: SELECT project by id
    alt Not found
      CTRL-->>C: 404 Not Found
      CTRL->>DB: ROLLBACK
    else Found
      CTRL->>DB: UPDATE projects SET status, last_updated, last_updated_by
      alt Update success
        DB-->>CTRL: success
        CTRL->>DB: COMMIT
        CTRL-->>C: 200 OK (updated project)
      else Update error
        DB-->>CTRL: error
        CTRL->>DB: ROLLBACK
        CTRL-->>C: 500 Internal Server Error
      end
    end
  end
  note over CTRL,DB: Logs emitted at start, not-found, success, and failure
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my nose and hop to code,
enums in rows, migrations strode.
Patch a status, commit the cheer,
tenants waking, updates clear.
A carrot curl — the project glows. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The current title includes an unclear shorthand (“Yb sep 22”) and does not clearly convey the addition of project status support to the organizational backend, making it difficult for reviewers to quickly grasp the primary change. Rename the pull request title to clearly and concisely summarize the main change, for example “Add project status tracking API and DB migration for organizational backend.”
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues Check ✅ Passed The PR fully implements the backend requirements from issue #2157 by adding the status enum, database migration, model updates, API endpoint, utility changes, and tenant script, and it aligns with the issue guidance that backend and UI work may be split into separate PRs.
Out of Scope Changes Check ✅ Passed All modifications—including the migration, enum, model, interface, controller, route, and utility updates—directly support the project status tracking feature and none of the changes venture outside the scope of the linked issue.
Description Check ✅ Passed The description follows the repository template by providing a clear summary of changes, including the “Fixes #2157” reference and the required checklist section, and it even includes a UI screenshot for context.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch yb-sep-22-statuses-to-organizational-backend

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d734b2 and ae8cd9a.

📒 Files selected for processing (3)
  • Servers/controllers/project.ctrl.ts (2 hunks)
  • Servers/scripts/createNewTenant.ts (1 hunks)
  • Servers/utils/project.utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Servers/scripts/createNewTenant.ts
  • Servers/utils/project.utils.ts
  • Servers/controllers/project.ctrl.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Servers/utils/project.utils.ts (2)

355-367: Critical: update-path deletes all members when callers pass [] (e.g., status-only update).

This block treats the incoming members array as the complete desired set. Callers like updateProjectStatus pass [], which will delete all existing members.

Apply this diff to make membership updates opt‑in only and avoid destructive deletes unless members is explicitly provided:

-export const updateProjectByIdQuery = async (
+export const updateProjectByIdQuery = async (
   id: number,
-  project: Partial<ProjectModel>,
-  members: number[],
+  project: Partial<ProjectModel>,
+  members: number[] | undefined,
   tenant: string,
   transaction: Transaction
 ): Promise<(IProjectAttributes & { members: number[] }) | null> => {
-  const _currentMembers = await sequelize.query(
+  const _currentMembers = await sequelize.query(
     `SELECT user_id FROM "${tenant}".projects_members WHERE project_id = :project_id`,
     {
       replacements: { project_id: id },
       mapToModel: true,
       model: ProjectsMembersModel,
       transaction,
     }
   );
   const currentMembers = _currentMembers.map((m) => m.user_id);
-  const deletedMembers = currentMembers.filter((m) => !members.includes(m));
-  const newMembers = members.filter((m) => !currentMembers.includes(m));
+  const deletedMembers = Array.isArray(members) ? currentMembers.filter((m) => !members.includes(m)) : [];
+  const newMembers = Array.isArray(members) ? members.filter((m) => !currentMembers.includes(m)) : [];

394-417: Filtering out “falsy” values prevents legitimate updates (e.g., empty string, 0, false).

The predicate project[f] !== undefined && project[f] skips updates to falsy values. Use an undefined‑only check.

Apply:

-  ]
-    .filter((f) => {
-      if (
-        project[f as keyof ProjectModel] !== undefined &&
-        project[f as keyof ProjectModel]
-      ) {
-        updateProject[f as keyof ProjectModel] =
-          project[f as keyof ProjectModel];
-        return true;
-      }
-    })
+  ]
+    .filter((f) => project[f as keyof ProjectModel] !== undefined)
+    .map((f) => {
+      updateProject[f as keyof ProjectModel] = project[f as keyof ProjectModel];
+      return f;
+    })
Servers/routes/project.route.ts (1)

36-37: Route order bug: '/:id' shadows '/stats/:id'.

As registered, GET /stats/123 will match '/:id' first. Move '/:id' after the more specific routes.

Apply:

-router.get("/:id", authenticateJWT, getProjectById);
-router.get("/stats/:id", authenticateJWT, getProjectStatsById);
+router.get("/stats/:id", authenticateJWT, getProjectStatsById);
+router.get("/:id", authenticateJWT, getProjectById);
Servers/controllers/project.ctrl.ts (1)

339-353: getProjectStatsById uses unresolved promises (missing await).

ownerUser and userWhoUpdated are Promises; properties like name/surname will be undefined at runtime.

Suggested fix (illustrative):

-    const ownerUser: any = getUserByIdQuery(project_owner);
+    const ownerUser: any = await getUserByIdQuery(project_owner);
...
-    const userWhoUpdated: any = getUserByIdQuery(project_last_updated_by);
+    const userWhoUpdated: any = await getUserByIdQuery(project_last_updated_by);
🧹 Nitpick comments (5)
Servers/utils/project.utils.ts (1)

419-429: Guard against empty SET clause.

If no fields are provided (and members are undefined), this builds UPDATE ... SET WHERE ..., which is invalid SQL.

Suggested minimal guard:

-  const query = `UPDATE "${tenant}".projects SET ${setClause} WHERE id = :id RETURNING *;`;
+  if (!setClause) {
+    // No field updates; return current row with updated members
+    const current = await getProjectByIdQuery(id, tenant);
+    const updatedMembers = await sequelize.query(
+      `SELECT user_id FROM "${tenant}".projects_members WHERE project_id = :project_id`,
+      { replacements: { project_id: id }, mapToModel: true, model: ProjectsMembersModel, transaction }
+    );
+    return current
+      ? { ...(current as any).dataValues ?? current, members: updatedMembers.map((m: any) => m.user_id) }
+      : null;
+  }
+  const query = `UPDATE "${tenant}".projects SET ${setClause} WHERE id = :id RETURNING *;`;
Servers/routes/project.route.ts (1)

39-39: Typo in path segment.

'complainces' → 'compliances' (introduces a public API surface inconsistency).

Suggested:

-router.get("/complainces/:projid", authenticateJWT, getCompliances);
+router.get("/compliances/:projid", authenticateJWT, getCompliances);
Servers/domain.layer/interfaces/i.project.ts (1)

19-20: Interface extension looks good; optional on write, present on read.

status?: ProjectStatus is fine for create/update. Consider documenting that reads will always include a non‑null status.

Servers/controllers/project.ctrl.ts (2)

329-360: Unrelated but important: fileName typos in logs ('projec.ctrl.ts').

Multiple occurrences; keep logs consistent for observability.


410-441: Return codes: consider 200 instead of 202 for GET endpoints.

These endpoints return data immediately; 200 is more conventional for successful GETs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0c365c and 4903d3b.

📒 Files selected for processing (7)
  • Servers/controllers/project.ctrl.ts (2 hunks)
  • Servers/database/migrations/20250922000001-add-project-status.js (1 hunks)
  • Servers/domain.layer/enums/project-status.enum.ts (1 hunks)
  • Servers/domain.layer/interfaces/i.project.ts (2 hunks)
  • Servers/domain.layer/models/project/project.model.ts (2 hunks)
  • Servers/routes/project.route.ts (2 hunks)
  • Servers/utils/project.utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
Servers/domain.layer/enums/project-status.enum.ts (1)
Clients/src/application/hooks/useProjectStatus.tsx (1)
  • ProjectStatus (46-49)
Servers/domain.layer/interfaces/i.project.ts (1)
Clients/src/application/hooks/useProjectStatus.tsx (1)
  • ProjectStatus (46-49)
Servers/database/migrations/20250922000001-add-project-status.js (1)
Servers/tools/getTenantHash.ts (1)
  • getTenantHash (3-6)
Servers/domain.layer/models/project/project.model.ts (1)
Clients/src/application/hooks/useProjectStatus.tsx (1)
  • ProjectStatus (46-49)
Servers/controllers/project.ctrl.ts (2)
Servers/utils/logger/logHelper.ts (3)
  • logProcessing (22-30)
  • logSuccess (33-49)
  • logFailure (51-68)
Servers/utils/project.utils.ts (2)
  • getProjectByIdQuery (114-159)
  • updateProjectByIdQuery (348-446)
Servers/routes/project.route.ts (1)
Servers/controllers/project.ctrl.ts (1)
  • updateProjectStatus (730-799)
🔇 Additional comments (8)
Servers/domain.layer/models/project/project.model.ts (2)

12-12: Import is correct and scoped.

Matches the enum usage in this file and across the stack.


89-95: Add status column — enums are in sync with the DB migration.
ProjectStatus in Servers/domain.layer/enums/project-status.enum.ts matches Servers/database/migrations/20250922000001-add-project-status.js (ENUM values and default 'Not started').

Servers/routes/project.route.ts (2)

69-70: Status route added — good.

PATCH /:id/status is a clear, narrowly scoped endpoint.


19-20: Importing updateProjectStatus — OK.

Matches the new PATCH route wiring.

Servers/domain.layer/interfaces/i.project.ts (1)

3-3: Import is correct and consistent.

Servers/database/migrations/20250922000001-add-project-status.js (1)

26-65: Transactional DDL across all tenants — OK, but monitor lock time.

Single long transaction is safer for rollback but may extend locks during peak hours. Plan deployment during low traffic.

Servers/controllers/project.ctrl.ts (1)

28-28: Import of ProjectStatus is correct.

Servers/domain.layer/enums/project-status.enum.ts (1)

1-9: Confirm canonical "Closed" wording and adopt machine tokens

  • Repo search: no "Closed (archived)" found. Backend enum uses "Closed" (Servers/domain.layer/enums/project-status.enum.ts); client maps Canceled -> "Closed" (Clients/src/domain/enums/mitigitaion.ts:7).
  • Recommendation: agree a canonical label in the product spec, persist a machine‑friendly token (e.g., CLOSED) in the DB, and render the human label (e.g., "Closed (archived)") in the UI/i18n layer to avoid future migrations.

Comment on lines +741 to +760
try {
// Validate status value
if (!status || !Object.values(ProjectStatus).includes(status)) {
return res.status(400).json(
STATUS_CODE[400]({ message: "Valid status is required" })
);
}

// Check if project exists
const existingProject = await getProjectByIdQuery(projectId, req.tenantId!);
if (!existingProject) {
await logSuccess({
eventType: "Update",
description: `Project not found for status update: ID ${projectId}`,
functionName: "updateProjectStatus",
fileName: "project.ctrl.ts",
});

return res.status(404).json(STATUS_CODE[404]({}));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Authorization check missing for status updates.

Endpoint requires JWT but does not verify project ownership/membership/admin role. Add a guard to prevent arbitrary status changes across tenants.

Suggested snippet:

+    // Authorization: only owner, project member, or Admin can update
+    const { userId, role } = req;
+    const isAdmin = role === 'Admin';
+    const isMember = isAdmin ? true : await sequelize.query(
+      `SELECT 1 FROM "${req.tenantId}".projects_members WHERE project_id = :project_id AND user_id = :user_id LIMIT 1`,
+      { replacements: { project_id: projectId, user_id: userId }, type: QueryTypes.SELECT }
+    ).then(r => r.length > 0);
+    if (!isMember) {
+      return res.status(403).json(STATUS_CODE[403]({ message: "Forbidden" }));
+    }
📝 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
try {
// Validate status value
if (!status || !Object.values(ProjectStatus).includes(status)) {
return res.status(400).json(
STATUS_CODE[400]({ message: "Valid status is required" })
);
}
// Check if project exists
const existingProject = await getProjectByIdQuery(projectId, req.tenantId!);
if (!existingProject) {
await logSuccess({
eventType: "Update",
description: `Project not found for status update: ID ${projectId}`,
functionName: "updateProjectStatus",
fileName: "project.ctrl.ts",
});
return res.status(404).json(STATUS_CODE[404]({}));
}
try {
// Validate status value
if (!status || !Object.values(ProjectStatus).includes(status)) {
return res.status(400).json(
STATUS_CODE[400]({ message: "Valid status is required" })
);
}
// Authorization: only owner, project member, or Admin can update
const { userId, role } = req;
const isAdmin = role === 'Admin';
const isMember = isAdmin ? true : await sequelize.query(
`SELECT 1 FROM "${req.tenantId}".projects_members WHERE project_id = :project_id AND user_id = :user_id LIMIT 1`,
{ replacements: { project_id: projectId, user_id: userId }, type: QueryTypes.SELECT }
).then(r => r.length > 0);
if (!isMember) {
return res.status(403).json(STATUS_CODE[403]({ message: "Forbidden" }));
}
// Check if project exists
const existingProject = await getProjectByIdQuery(projectId, req.tenantId!);
if (!existingProject) {
await logSuccess({
eventType: "Update",
description: `Project not found for status update: ID ${projectId}`,
functionName: "updateProjectStatus",
fileName: "project.ctrl.ts",
});
return res.status(404).json(STATUS_CODE[404]({}));
}
🤖 Prompt for AI Agents
In Servers/controllers/project.ctrl.ts around lines 741 to 760, the status
update path is missing an authorization guard allowing any authenticated JWT to
change a project's status; add a check that the requester is authorized by
verifying req.tenantId matches the project's tenant and that the user (from
req.user or req.userId/role) is either the project owner, an admin of that
tenant, or a project member with permission to change status; if not authorized
return res.status(403).json(STATUS_CODE[403]({})); include an audit log entry
for denied attempts and proceed only when the authorization check passes.

Comment on lines +763 to +769
const updatedProject = await updateProjectByIdQuery(
projectId,
{ status, last_updated: new Date(), last_updated_by: req.userId! },
[], // no members update
req.tenantId!,
transaction
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Passing [] for members will purge all project members.

When calling updateProjectByIdQuery with [], the util interprets it as “no members should remain.” Use undefined to signify “do not change members,” paired with the util change.

Apply:

-    const updatedProject = await updateProjectByIdQuery(
+    const updatedProject = await updateProjectByIdQuery(
       projectId,
       { status, last_updated: new Date(), last_updated_by: req.userId! },
-      [], // no members update
+      undefined, // do not modify members
       req.tenantId!,
       transaction
     );

And ensure Servers/utils/project.utils.ts is updated per the related comment.

📝 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
const updatedProject = await updateProjectByIdQuery(
projectId,
{ status, last_updated: new Date(), last_updated_by: req.userId! },
[], // no members update
req.tenantId!,
transaction
);
const updatedProject = await updateProjectByIdQuery(
projectId,
{ status, last_updated: new Date(), last_updated_by: req.userId! },
undefined, // do not modify members
req.tenantId!,
transaction
);
🤖 Prompt for AI Agents
In Servers/controllers/project.ctrl.ts around lines 763 to 769, the call to
updateProjectByIdQuery currently passes an empty array for members which will
remove all project members; change that argument to undefined to indicate “do
not change members” and leave members untouched, and update
Servers/utils/project.utils.ts so the util treats undefined as no-op (no member
changes) while still treating an empty array as an explicit clear of members.

@@ -0,0 +1,105 @@
'use strict';
const { getTenantHash } = require("../../dist/tools/getTenantHash");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not require build artifacts from migrations.

Requiring from ../../dist makes migrations environment‑dependent and brittle (dist may not exist in CI/production). Inline the helper or import from a JS util that lives with migrations.

Apply:

-'use strict';
-const { getTenantHash } = require("../../dist/tools/getTenantHash");
+'use strict';
+const crypto = require("crypto");
+function getTenantHash(tenantId) {
+  const hash = crypto.createHash('sha256').update(String(tenantId)).digest('base64');
+  return hash.replace(/[^a-zA-Z0-9]/g, '').slice(0, 10);
+}
📝 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
const { getTenantHash } = require("../../dist/tools/getTenantHash");
'use strict';
const crypto = require("crypto");
function getTenantHash(tenantId) {
const hash = crypto.createHash('sha256').update(String(tenantId)).digest('base64');
return hash.replace(/[^a-zA-Z0-9]/g, '').slice(0, 10);
}
🤖 Prompt for AI Agents
In Servers/database/migrations/20250922000001-add-project-status.js around line
2, the migration currently requires a helper from ../../dist/tools/getTenantHash
which depends on build artifacts; replace that by either inlining the
getTenantHash implementation directly into this migration or moving the helper
into a source file alongside migrations (e.g.,
Servers/database/migrations/utils/getTenantHash.js) and require that relative JS
module instead; update the require to point to the new local util or remove the
external require and paste the helper code here, ensuring the migration no
longer depends on ../../dist so it can run in unbuilt environments.

Comment on lines +74 to +105
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// Remove status column from public.projects table
await queryInterface.removeColumn('projects', 'status', { transaction });

// Get all organizations to update their tenant schemas
const organizations = await queryInterface.sequelize.query(
`SELECT id FROM public.organizations;`, { transaction }
);

// Remove status column from each tenant's projects table
for (let organization of organizations[0]) {
const tenantHash = getTenantHash(organization.id);

await queryInterface.removeColumn(
{
tableName: 'projects',
schema: tenantHash
},
'status',
{ transaction }
);
}

await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
}; No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Drop the generated ENUM types on down().

Sequelize creates enum types (e.g., enum_projects_status) per schema. Removing columns leaves orphaned types.

Apply:

   async down(queryInterface, Sequelize) {
     const transaction = await queryInterface.sequelize.transaction();
     try {
-      // Remove status column from public.projects table
+      // Remove status column from public.projects table
       await queryInterface.removeColumn('projects', 'status', { transaction });
+      // Drop public enum type if present
+      await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_projects_status";', { transaction });
 
       // Get all organizations to update their tenant schemas
       const organizations = await queryInterface.sequelize.query(
         `SELECT id FROM public.organizations;`, { transaction }
       );
 
       // Remove status column from each tenant's projects table
       for (let organization of organizations[0]) {
         const tenantHash = getTenantHash(organization.id);
-        
+
         await queryInterface.removeColumn(
           {
             tableName: 'projects',
             schema: tenantHash
           },
           'status',
           { transaction }
         );
+        // Drop tenant-scoped enum type
+        await queryInterface.sequelize.query(
+          `DROP TYPE IF EXISTS "${tenantHash}"."enum_projects_status";`,
+          { transaction }
+        );
       }
 
       await transaction.commit();
📝 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
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// Remove status column from public.projects table
await queryInterface.removeColumn('projects', 'status', { transaction });
// Get all organizations to update their tenant schemas
const organizations = await queryInterface.sequelize.query(
`SELECT id FROM public.organizations;`, { transaction }
);
// Remove status column from each tenant's projects table
for (let organization of organizations[0]) {
const tenantHash = getTenantHash(organization.id);
await queryInterface.removeColumn(
{
tableName: 'projects',
schema: tenantHash
},
'status',
{ transaction }
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// Remove status column from public.projects table
await queryInterface.removeColumn('projects', 'status', { transaction });
// Drop public enum type if present
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_projects_status";', { transaction });
// Get all organizations to update their tenant schemas
const organizations = await queryInterface.sequelize.query(
`SELECT id FROM public.organizations;`, { transaction }
);
// Remove status column from each tenant's projects table
for (let organization of organizations[0]) {
const tenantHash = getTenantHash(organization.id);
await queryInterface.removeColumn(
{
tableName: 'projects',
schema: tenantHash
},
'status',
{ transaction }
);
// Drop tenant-scoped enum type
await queryInterface.sequelize.query(
`DROP TYPE IF EXISTS "${tenantHash}"."enum_projects_status";`,
{ transaction }
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};
🤖 Prompt for AI Agents
In Servers/database/migrations/20250922000001-add-project-status.js around lines
74 to 105, the down() flow removes the projects.status column but leaves behind
Sequelize-generated ENUM types (e.g., enum_projects_status) in public and tenant
schemas; after removing the column(s) you must explicitly drop the enum type
within the same transaction to avoid orphaned types — run a DROP TYPE IF EXISTS
"enum_projects_status" (qualified in the target schema) for public and for each
tenant schema (use the tenantHash to qualify the schema or run SET search_path)
inside the loop and after the public removeColumn, ensuring all DROP TYPE
queries execute with the same transaction and rollback on error.

@MuhammadKhalilzadeh
Copy link
Copy Markdown
Collaborator

@Aryanak47 You have a conflict in here now

…into yb-sep-22-statuses-to-organizational-backend
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
Servers/utils/project.utils.ts (2)

348-354: Make members optional to avoid accidental purges.

Passing undefined should mean “don’t change members.” Current required param leads to mass‑removals when callers pass [] by default.

Apply:

 export const updateProjectByIdQuery = async (
   id: number,
   project: Partial<ProjectModel>,
-  members: number[],
+  members?: number[],
   tenant: string,
   transaction: Transaction
 ): Promise<(IProjectAttributes & { members: number[] }) | null> => {

355-392: Guard member updates only when members is provided.

Wrap the membership diff logic so that undefined skips updates; empty array still intentionally clears membership.

-  const _currentMembers = await sequelize.query(
-    `SELECT user_id FROM "${tenant}".projects_members WHERE project_id = :project_id`,
-    {
-      replacements: { project_id: id },
-      mapToModel: true,
-      model: ProjectsMembersModel,
-      transaction,
-    }
-  );
-  const currentMembers = _currentMembers.map((m) => m.user_id);
-  const deletedMembers = currentMembers.filter((m) => !members.includes(m));
-  const newMembers = members.filter((m) => !currentMembers.includes(m));
-
-  for (let member of deletedMembers) {
-    await sequelize.query(
-      `DELETE FROM "${tenant}".projects_members WHERE user_id = :user_id AND project_id = :project_id`,
-      {
-        replacements: { user_id: member, project_id: id },
-        mapToModel: true,
-        model: ProjectsMembersModel,
-        type: QueryTypes.DELETE,
-        transaction,
-      }
-    );
-  }
-
-  for (let member of newMembers) {
-    await sequelize.query(
-      `INSERT INTO "${tenant}".projects_members (project_id, user_id) VALUES (:project_id, :user_id);`,
-      {
-        replacements: { user_id: member, project_id: id },
-        mapToModel: true,
-        model: ProjectsMembersModel,
-        // type: QueryTypes.INSERT
-        transaction,
-      }
-    );
-  }
+  if (members !== undefined) {
+    const _currentMembers = await sequelize.query(
+      `SELECT user_id FROM "${tenant}".projects_members WHERE project_id = :project_id`,
+      {
+        replacements: { project_id: id },
+        mapToModel: true,
+        model: ProjectsMembersModel,
+        transaction,
+      }
+    );
+    const currentMembers = _currentMembers.map((m) => m.user_id);
+    const deletedMembers = currentMembers.filter((m) => !members.includes(m));
+    const newMembers = members.filter((m) => !currentMembers.includes(m));
+
+    for (let member of deletedMembers) {
+      await sequelize.query(
+        `DELETE FROM "${tenant}".projects_members WHERE user_id = :user_id AND project_id = :project_id`,
+        {
+          replacements: { user_id: member, project_id: id },
+          mapToModel: true,
+          model: ProjectsMembersModel,
+          type: QueryTypes.DELETE,
+          transaction,
+        }
+      );
+    }
+
+    for (let member of newMembers) {
+      await sequelize.query(
+        `INSERT INTO "${tenant}".projects_members (project_id, user_id) VALUES (:project_id, :user_id);`,
+        {
+          replacements: { user_id: member, project_id: id },
+          mapToModel: true,
+          model: ProjectsMembersModel,
+          // type: QueryTypes.INSERT
+          transaction,
+        }
+      );
+    }
+  }
Servers/controllers/project.ctrl.ts (1)

1-30: Add QueryTypes import for authorization query.

 import { Request, Response } from "express";
+import { QueryTypes } from "sequelize";
🧹 Nitpick comments (1)
Servers/utils/project.utils.ts (1)

406-416: Don’t drop falsy but valid updates (''/0/false/null).

Current truthy check skips legitimate updates. Prefer explicit undefined check and presence test.

-  .filter((f) => {
-      if (
-        project[f as keyof ProjectModel] !== undefined &&
-        project[f as keyof ProjectModel]
-      ) {
-        updateProject[f as keyof ProjectModel] =
-          project[f as keyof ProjectModel];
-        return true;
-      }
-    })
+  .filter((f) => {
+    if (
+      Object.prototype.hasOwnProperty.call(project, f) &&
+      project[f as keyof ProjectModel] !== undefined
+    ) {
+      updateProject[f as keyof ProjectModel] = project[f as keyof ProjectModel];
+      return true;
+    }
+    return false;
+  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4903d3b and 1d0566f.

📒 Files selected for processing (2)
  • Servers/controllers/project.ctrl.ts (2 hunks)
  • Servers/utils/project.utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Servers/controllers/project.ctrl.ts (2)
Servers/utils/logger/logHelper.ts (3)
  • logProcessing (22-30)
  • logSuccess (33-49)
  • logFailure (51-68)
Servers/utils/project.utils.ts (2)
  • getProjectByIdQuery (114-159)
  • updateProjectByIdQuery (348-446)
🔇 Additional comments (4)
Servers/utils/project.utils.ts (1)

404-405: Including status in updatable fields looks good.

Servers/controllers/project.ctrl.ts (3)

28-28: Import of ProjectStatus enum is correct.


783-785: Passing [] will purge all members; use undefined.

-      [], // no members update
+      undefined, // do not modify members

746-816: Missing authorization guard; any authenticated user can update status.

Enforce owner/member/Admin check within tenant before updating. Also add a 401 guard and use the transaction for the membership check.

 export async function updateProjectStatus(req: Request, res: Response): Promise<any> {
   const transaction = await sequelize.transaction();
   const projectId = parseInt(req.params.id);
   const { status } = req.body;

   logProcessing({
     description: `starting updateProjectStatus for ID ${projectId}`,
     functionName: "updateProjectStatus",
     fileName: "project.ctrl.ts",
   });

   try {
+    // Require auth context
+    if (!req.userId || !req.role) {
+      await logFailure({
+        eventType: "Update",
+        description: "Unauthorized status update attempt",
+        functionName: "updateProjectStatus",
+        fileName: "project.ctrl.ts",
+        error: new Error("Unauthorized"),
+      });
+      await transaction.rollback();
+      return res.status(401).json({ message: "Unauthorized" });
+    }
+
     // Validate status value
     if (!status || !Object.values(ProjectStatus).includes(status)) {
       return res.status(400).json(
         STATUS_CODE[400]({ message: "Valid status is required" })
       );
     }

     // Check if project exists
     const existingProject = await getProjectByIdQuery(projectId, req.tenantId!);
     if (!existingProject) {
       await logSuccess({
         eventType: "Update",
         description: `Project not found for status update: ID ${projectId}`,
         functionName: "updateProjectStatus",
         fileName: "project.ctrl.ts",
       });

       return res.status(404).json(STATUS_CODE[404]({}));
     }
+
+    // Authorization: only Admin, owner, or project member
+    const isAdmin = req.role === "Admin";
+    const isOwner = existingProject.owner === req.userId;
+    let isMember = false;
+    if (!isAdmin && !isOwner) {
+      const membership = await sequelize.query(
+        `SELECT 1 FROM "${req.tenantId}".projects_members WHERE project_id = :project_id AND user_id = :user_id LIMIT 1`,
+        {
+          replacements: { project_id: projectId, user_id: req.userId },
+          type: QueryTypes.SELECT,
+          transaction,
+        }
+      );
+      isMember = (membership as any[]).length > 0;
+    }
+    if (!isAdmin && !isOwner && !isMember) {
+      await logFailure({
+        eventType: "Update",
+        description: `Forbidden: user ${req.userId} not authorized to update status for project ${projectId}`,
+        functionName: "updateProjectStatus",
+        fileName: "project.ctrl.ts",
+        error: new Error("Forbidden"),
+      });
+      await transaction.rollback();
+      return res.status(403).json(STATUS_CODE[403]({ message: "Forbidden" }));
+    }

     // Update project status
     const updatedProject = await updateProjectByIdQuery(
       projectId,
       { status, last_updated: new Date(), last_updated_by: req.userId! },
-      [], // no members update
+      undefined, // do not modify members
       req.tenantId!,
       transaction
     );

@MuhammadKhalilzadeh
Copy link
Copy Markdown
Collaborator

@HarshP4585 Would you please provide your feedback here asap? We need to merge this. It's been open for a while now,

Copy link
Copy Markdown
Collaborator

@HarshP4585 HarshP4585 left a comment

Choose a reason for hiding this comment

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

Everything looks good, just need to update createNewTenant script to include status field in projects

@Aryanak47
Copy link
Copy Markdown
Contributor Author

Everything looks good, just need to update createNewTenant script to include status field in projects

I have just updated 'createNewTenant' script.

@MuhammadKhalilzadeh
Copy link
Copy Markdown
Collaborator

Everything looks good, just need to update createNewTenant script to include status field in projects

I have just updated 'createNewTenant' script.

@HarshP4585 Would you do a quick double check please?

Copy link
Copy Markdown
Member

@Br0wnHammer Br0wnHammer left a comment

Choose a reason for hiding this comment

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

LGTM! Great work 🚀

Copy link
Copy Markdown
Collaborator

@MuhammadKhalilzadeh MuhammadKhalilzadeh left a comment

Choose a reason for hiding this comment

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

Thank you so much @Aryanak47

@MuhammadKhalilzadeh MuhammadKhalilzadeh merged commit 06583e7 into develop Sep 29, 2025
2 checks passed
@MuhammadKhalilzadeh MuhammadKhalilzadeh deleted the yb-sep-22-statuses-to-organizational-backend branch September 29, 2025 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add statuses to organizational projects

4 participants