Skip to content

Conversation

@devksingh4
Copy link
Member

@devksingh4 devksingh4 commented Oct 24, 2025

This allows us to have some people who are not exec representatives but do have org admin privileges.

Summary by CodeRabbit

  • New Features

    • Exec Council membership is now synced asynchronously via a background workflow; a new background sync operation can be triggered.
  • Refactor

    • Voting-lead discovery now aggregates leads across organizations with stronger consistency and expanded lead details.
    • GitHub team parent reference made optional and parent-team config key renamed for clarity.
  • Style

    • Non-voting member wording updated to reference the specific organization.
  • Tests

    • Added unit tests covering mixed voting/non-voting lead scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds an SQS-driven Exec Council sync handler and payload, replaces dynamic per-org lead queries with parallel ConsistentRead queries, renames ExecGithubTeam → OrgAdminGithubParentTeam in config, makes GitHub team parentTeamId optional, and enqueues Exec Council syncs after lead updates.

Changes

Cohort / File(s) Summary
Config & SQS types
src/common/config.ts, src/common/types/sqsMessage.ts
Renamed ExecGithubTeamOrgAdminGithubParentTeam in config; added AvailableSQSFunctions.SyncExecCouncil, an empty payload schema, and included it in the discriminated union.
GitHub API inputs
src/api/functions/github.ts
Made parentTeamId optional in CreateGithubTeamInputs (parentTeamId?: number).
Organization lead logic
src/api/functions/organizations.ts
Replaced getLeadsM365DynamicQuery with getAllVotingLeads returning an array of lead objects (now includes nonVotingMember); added shouldBeInExecCouncil; switched to parallel per-org ConsistentRead queries and tightened error handling/logging.
Routes
src/api/routes/organizations.ts
Removed immediate dynamic membership update logic; now enqueues Exec Council sync via SQS after lead changes; adjusted imports accordingly.
SQS handlers
src/api/sqs/handlers/syncExecCouncil.ts, src/api/sqs/handlers/createOrgGithubTeam.ts, src/api/sqs/handlers/index.ts
Added syncExecCouncilHandler to reconcile Entra group members vs voting leads (compute adds/removes, per-item handling); changed createOrgGithubTeam to use OrgAdminGithubParentTeam; exported new handler from handlers index.
SQS wiring
src/api/sqs/index.ts
Wired AvailableSQSFunctions.SyncExecCouncil to syncExecCouncilHandler in the SQS dispatch map; switched imports to handlers barrel.
UI text
src/ui/pages/organization/ManageOrganizationForm.tsx
Non-voting checkbox and alert text now interpolate organizationId (organization-specific messaging).
Tests
tests/unit/organizations.test.ts
Added tests covering leads with varying nonVotingMember values and defaults; updated DynamoDB mocks to simulate mixed lead configurations.

Sequence Diagram(s)

sequenceDiagram
    participant Route as Org Route
    participant SQS as SQS
    participant Handler as syncExecCouncilHandler
    participant DDB as DynamoDB
    participant Entra as Entra ID

    Route->>SQS: Queue SyncExecCouncil
    SQS->>Handler: Dequeue & invoke
    Handler->>DDB: Parallel per-org ConsistentRead queries
    DDB-->>Handler: Voting leads array (incl. nonVotingMember)
    Handler->>Entra: Fetch current group members
    Entra-->>Handler: Current members list
    Handler->>Handler: Compute adds / removes
    alt add members
        Handler->>Entra: Add missing members (iterative)
    end
    alt remove members
        Handler->>Entra: Remove non-voting members (iterative)
    end
    Handler-->>SQS: Log summary / metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • src/api/functions/organizations.ts (new return types, parallel query logic, error handling)
    • src/api/sqs/handlers/syncExecCouncil.ts (Entra interactions, add/remove flows, per-item error handling)
    • src/common/config.ts and all callers for renamed config field
    • Call sites of CreateGithubTeamInputs to ensure optional parentTeamId is handled
    • SQS wiring and handler exports: src/api/sqs/index.ts, src/api/sqs/handlers/index.ts

Poem

🐰 I hopped through queues and lists with care,

Collected leads from every lair,
I nudged the group to add and prune,
Synced in the night beneath the moon,
A joyful rabbit hums — all clear!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Seperate Exec membership from Org admin" directly aligns with the main architectural change in the changeset. The modifications across multiple files implement the core objective of decoupling executive council membership logic from organization admin privileges, including refactoring how voting leads are determined (replacing getLeadsM365DynamicQuery with getAllVotingLeads), introducing an SQS-based async workflow for exec council sync, renaming the config field from ExecGithubTeam to OrgAdminGithubParentTeam, and updating UI text to reflect org-specific non-voting member handling. The title clearly summarizes this primary change and would help a teammate scanning the history quickly understand the nature of this refactoring.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 0a12700 and 2022c0a.

📒 Files selected for processing (1)
  • tests/unit/organizations.test.ts (1 hunks)

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

@github-actions
Copy link
Contributor

github-actions bot commented Oct 24, 2025

💰 Infracost report

Monthly estimate generated

This comment will be updated when code changes.

Copy link
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: 5

Caution

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

⚠️ Outside diff range comments (1)
src/api/functions/github.ts (1)

6-15: Make parent_team_id conditional when parentTeamId is undefined.

Now that parentTeamId is optional, avoid sending parent_team_id: undefined which can 4xx.

-    const response = await octokit.request("POST /orgs/{org}/teams", {
-      org: orgId,
-      name,
-      description: `[Managed by Core API]${description ? ` ${description}` : ""}`,
-      privacy: privacy || "closed",
-      notification_setting: "notifications_enabled",
-      parent_team_id: parentTeamId,
-    });
+    const body: Record<string, any> = {
+      org: orgId,
+      name,
+      description: `[Managed by Core API]${description ? ` ${description}` : ""}`,
+      privacy: privacy || "closed",
+      notification_setting: "notifications_enabled",
+    };
+    if (parentTeamId != null) body.parent_team_id = parentTeamId;
+    const response = await octokit.request("POST /orgs/{org}/teams", body);
🧹 Nitpick comments (2)
src/api/sqs/handlers/syncExecCouncil.ts (1)

1-20: import/extensions lints on “.js” paths.

If ESM with NodeNext, keep “.js” and relax ESLint; otherwise drop extensions.

src/api/functions/organizations.ts (1)

588-630: Prefer GetItem (or BatchGetItem) over Query for exact PK+SK; lower latency/RCUs

You’re querying by full primary key (partition + sort). GetItem with ConsistentRead is the direct, cheaper call and simplifies the code. BatchGetItem could fetch all org keys in one request if desired.

Add import (outside this hunk):

+import { GetItemCommand } from "@aws-sdk/client-dynamodb";

Refactor body:

   for (const orgName of AllOrganizationNameList) {
-    const leadsQuery = new QueryCommand({
-      TableName: genericConfig.SigInfoTableName,
-      KeyConditionExpression: "primaryKey = :leadName AND entryId = :username",
-      ExpressionAttributeValues: {
-        ":leadName": { S: `LEAD#${orgName}` },
-        ":username": { S: username },
-      },
-      ConsistentRead: true,
-    });
-
-    try {
-      const responseMarshall = await dynamoClient.send(leadsQuery);
-      if (responseMarshall.Items && responseMarshall.Items.length > 0) {
-        const lead = unmarshall(responseMarshall.Items[0]);
-        // If they're a lead and not a non-voting member, they should be in exec
-        if (!lead.nonVotingMember) {
-          return true;
-        }
-      }
-    } catch (e) {
+    try {
+      const res = await dynamoClient.send(
+        new GetItemCommand({
+          TableName: genericConfig.SigInfoTableName,
+          Key: marshall({
+            primaryKey: `LEAD#${orgName}`,
+            entryId: username,
+          }),
+          ConsistentRead: true,
+          ProjectionExpression: "nonVotingMember",
+        }),
+      );
+      if (res.Item) {
+        const lead = unmarshall(res.Item);
+        if (!lead.nonVotingMember) return true;
+      }
+    } catch (e) {
       if (e instanceof BaseError) {
         throw e;
       }
       logger.error(e);
       throw new DatabaseFetchError({
         message: `Failed to check lead status for ${username} in org ${orgName}.`,
       });
     }
   }

Optional: replace the loop with a single BatchGetItem across all orgs for a single network round trip.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between ae5acf5 and 8c268b8.

📒 Files selected for processing (9)
  • src/api/functions/github.ts (1 hunks)
  • src/api/functions/organizations.ts (1 hunks)
  • src/api/routes/organizations.ts (4 hunks)
  • src/api/sqs/handlers/createOrgGithubTeam.ts (1 hunks)
  • src/api/sqs/handlers/index.ts (1 hunks)
  • src/api/sqs/handlers/syncExecCouncil.ts (1 hunks)
  • src/api/sqs/index.ts (2 hunks)
  • src/common/config.ts (3 hunks)
  • src/common/types/sqsMessage.ts (3 hunks)
🧰 Additional context used
🪛 ESLint
src/api/sqs/handlers/index.ts

[error] 7-7: Unexpected use of file extension "js" for "./syncExecCouncil.js"

(import/extensions)

src/api/sqs/handlers/syncExecCouncil.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/ySwLhLIcEr'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 1-1: Unexpected use of file extension "js" for "common/types/sqsMessage.js"

(import/extensions)


[error] 6-6: Unexpected use of file extension "js" for "../index.js"

(import/extensions)


[error] 12-12: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/organizations.js"

(import/extensions)


[error] 18-18: Unexpected use of file extension "js" for "api/functions/entraId.js"

(import/extensions)


[error] 20-20: Unexpected use of file extension "js" for "common/types/iam.js"

(import/extensions)

src/common/types/sqsMessage.ts

[error] 83-83: Replace ·z.object({}) with ⏎····z.object({}),

(prettier/prettier)


[error] 102-102: Insert ,

(prettier/prettier)

src/api/routes/organizations.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/iDtwctVzgQ'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 42-42: Unexpected use of file extension "js" for "api/functions/entraId.js"

(import/extensions)


[error] 44-44: Unexpected use of file extension "js" for "api/functions/sts.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Application
  • GitHub Check: Run Unit Tests
🔇 Additional comments (7)
src/common/config.ts (2)

144-146: Confirm parent team IDs are correct in both envs.

If the parent team is ever not set for an env, callers should omit the parent instead of passing an undefined number. See suggested guard in github.ts comment.

Also applies to: 176-177


33-36: Verified: no stale ExecGithubTeam references found.

The rename to OrgAdminGithubParentTeam is complete—no orphaned references to the old key exist in the codebase.

src/common/types/sqsMessage.ts (1)

11-12: Enum addition is fine; ensure queue-level gating if needed.

If SyncExecCouncil should be limited to a specific SQS queue, add it to restrictedQueues in src/api/sqs/index.ts.

src/api/sqs/handlers/createOrgGithubTeam.ts (1)

92-96: Switch to OrgAdminGithubParentTeam looks correct.

No behavior change. Ensure this value exists for all envs; if an env intentionally omits a parent, the GitHub request should skip parent_team_id (see github.ts suggestion).

src/api/sqs/index.ts (1)

21-23: Wiring LGTM.

Dispatch table correctly maps CreateOrgGithubTeam and SyncExecCouncil. Consider queue gating for SyncExecCouncil if appropriate (restrictedQueues).

Also applies to: 45-46

src/api/routes/organizations.ts (2)

487-488: LGTM: Clear documentation of architectural change.

The comment effectively explains that exec council membership management has been moved from dynamic membership rules to an SQS-based sync handler, making the code easier to understand for future maintainers.


565-577: Exec council sync implementation is correct and properly handles the sync workflow.

The unconditional queuing ensures voting member changes are consistently reflected in the exec council. The handler correctly queries the source of truth (DynamoDB for voting leads, Entra ID for current members), performs selective adds/removes, and handles individual operation failures gracefully.

Comment on lines +533 to +579
export async function getAllVotingLeads({
dynamoClient,
includeGroupIds,
logger,
}: {
dynamoClient: DynamoDBClient;
includeGroupIds?: string[];
}): Promise<string | null> {
const command = new ScanCommand({
TableName: genericConfig.SigInfoTableName,
IndexName: "LeadsGroupIdIndex",
logger: ValidLoggers;
}): Promise<
Array<{ username: string; org: string; name: string; title: string }>
> {
// Query all organizations in parallel for better performance
const queryPromises = AllOrganizationNameList.map(async (orgName) => {
const leadsQuery = new QueryCommand({
TableName: genericConfig.SigInfoTableName,
KeyConditionExpression: "primaryKey = :leadName",
ExpressionAttributeValues: {
":leadName": { S: `LEAD#${orgName}` },
},
ConsistentRead: true,
});

try {
const responseMarshall = await dynamoClient.send(leadsQuery);
if (responseMarshall.Items) {
return responseMarshall.Items.map((x) => unmarshall(x))
.filter((x) => x.username && !x.nonVotingMember)
.map((x) => ({
username: x.username as string,
org: orgName,
name: x.name as string,
title: x.title as string,
}));
}
return [];
} catch (e) {
if (e instanceof BaseError) {
throw e;
}
logger.error(e);
throw new DatabaseFetchError({
message: `Failed to get leads for org ${orgName}.`,
});
}
});
const results = await dynamoClient.send(command);
if (!results || !results.Items || results.Items.length === 0) {
return null;
}
const entries = results.Items.map((x) => unmarshall(x)) as {
primaryKey: string;
leadsEntraGroupId: string;
}[];
const groupIds = entries
.filter((x) => x.primaryKey.startsWith("DEFINE#"))
.map((x) => x.leadsEntraGroupId);

if (groupIds.length === 0) {
return null;

const results = await Promise.all(queryPromises);
return results.flat();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix return type for title (can be undefined) + trim payload + optional resiliency

  • Title may be missing on some lead items; current return type promises string. Make it optional to avoid misleading consumers and potential runtime issues. Also project only needed attributes to cut payload. Optionally, return partial results if one org read fails.

Apply:

 export async function getAllVotingLeads({
   dynamoClient,
   logger,
 }: {
   dynamoClient: DynamoDBClient;
-  logger: ValidLoggers;
-}): Promise<
-  Array<{ username: string; org: string; name: string; title: string }>
-> {
+  logger: ValidLoggers;
+}): Promise<
+  Array<{ username: string; org: string; name: string; title?: string }>
+> {
   // Query all organizations in parallel for better performance
   const queryPromises = AllOrganizationNameList.map(async (orgName) => {
     const leadsQuery = new QueryCommand({
       TableName: genericConfig.SigInfoTableName,
       KeyConditionExpression: "primaryKey = :leadName",
       ExpressionAttributeValues: {
         ":leadName": { S: `LEAD#${orgName}` },
       },
+      ProjectionExpression: "#u, #n, title, nonVotingMember",
+      ExpressionAttributeNames: { "#u": "username", "#n": "name" },
       ConsistentRead: true,
     });
 
     try {
       const responseMarshall = await dynamoClient.send(leadsQuery);
       if (responseMarshall.Items) {
         return responseMarshall.Items.map((x) => unmarshall(x))
           .filter((x) => x.username && !x.nonVotingMember)
           .map((x) => ({
             username: x.username as string,
             org: orgName,
-            name: x.name as string,
-            title: x.title as string,
+            name: x.name as string,
+            title: x.title,
           }));
       }
       return [];
     } catch (e) {
       if (e instanceof BaseError) {
         throw e;
       }
       logger.error(e);
       throw new DatabaseFetchError({
         message: `Failed to get leads for org ${orgName}.`,
       });
     }
   });
 
-  const results = await Promise.all(queryPromises);
-  return results.flat();
+  // Option B (resiliency): tolerate one-org failures and return partial results
+  // const settled = await Promise.allSettled(queryPromises);
+  // const results: Array<{ username: string; org: string; name: string; title?: string }> = [];
+  // for (const s of settled) {
+  //   if (s.status === "fulfilled") results.push(...s.value);
+  //   else logger.error(s.reason);
+  // }
+  // return results;
+  const results = await Promise.all(queryPromises);
+  return results.flat();
 }
📝 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
export async function getAllVotingLeads({
dynamoClient,
includeGroupIds,
logger,
}: {
dynamoClient: DynamoDBClient;
includeGroupIds?: string[];
}): Promise<string | null> {
const command = new ScanCommand({
TableName: genericConfig.SigInfoTableName,
IndexName: "LeadsGroupIdIndex",
logger: ValidLoggers;
}): Promise<
Array<{ username: string; org: string; name: string; title: string }>
> {
// Query all organizations in parallel for better performance
const queryPromises = AllOrganizationNameList.map(async (orgName) => {
const leadsQuery = new QueryCommand({
TableName: genericConfig.SigInfoTableName,
KeyConditionExpression: "primaryKey = :leadName",
ExpressionAttributeValues: {
":leadName": { S: `LEAD#${orgName}` },
},
ConsistentRead: true,
});
try {
const responseMarshall = await dynamoClient.send(leadsQuery);
if (responseMarshall.Items) {
return responseMarshall.Items.map((x) => unmarshall(x))
.filter((x) => x.username && !x.nonVotingMember)
.map((x) => ({
username: x.username as string,
org: orgName,
name: x.name as string,
title: x.title as string,
}));
}
return [];
} catch (e) {
if (e instanceof BaseError) {
throw e;
}
logger.error(e);
throw new DatabaseFetchError({
message: `Failed to get leads for org ${orgName}.`,
});
}
});
const results = await dynamoClient.send(command);
if (!results || !results.Items || results.Items.length === 0) {
return null;
}
const entries = results.Items.map((x) => unmarshall(x)) as {
primaryKey: string;
leadsEntraGroupId: string;
}[];
const groupIds = entries
.filter((x) => x.primaryKey.startsWith("DEFINE#"))
.map((x) => x.leadsEntraGroupId);
if (groupIds.length === 0) {
return null;
const results = await Promise.all(queryPromises);
return results.flat();
}
export async function getAllVotingLeads({
dynamoClient,
logger,
}: {
dynamoClient: DynamoDBClient;
logger: ValidLoggers;
}): Promise<
Array<{ username: string; org: string; name: string; title?: string }>
> {
// Query all organizations in parallel for better performance
const queryPromises = AllOrganizationNameList.map(async (orgName) => {
const leadsQuery = new QueryCommand({
TableName: genericConfig.SigInfoTableName,
KeyConditionExpression: "primaryKey = :leadName",
ExpressionAttributeValues: {
":leadName": { S: `LEAD#${orgName}` },
},
ProjectionExpression: "#u, #n, title, nonVotingMember",
ExpressionAttributeNames: { "#u": "username", "#n": "name" },
ConsistentRead: true,
});
try {
const responseMarshall = await dynamoClient.send(leadsQuery);
if (responseMarshall.Items) {
return responseMarshall.Items.map((x) => unmarshall(x))
.filter((x) => x.username && !x.nonVotingMember)
.map((x) => ({
username: x.username as string,
org: orgName,
name: x.name as string,
title: x.title,
}));
}
return [];
} catch (e) {
if (e instanceof BaseError) {
throw e;
}
logger.error(e);
throw new DatabaseFetchError({
message: `Failed to get leads for org ${orgName}.`,
});
}
});
// Option B (resiliency): tolerate one-org failures and return partial results
// const settled = await Promise.allSettled(queryPromises);
// const results: Array<{ username: string; org: string; name: string; title?: string }> = [];
// for (const s of settled) {
// if (s.status === "fulfilled") results.push(...s.value);
// else logger.error(s.reason);
// }
// return results;
const results = await Promise.all(queryPromises);
return results.flat();
}
🤖 Prompt for AI Agents
In src/api/functions/organizations.ts around lines 533 to 579, the function
currently declares title as string, fetches full items, and throws if any org
query fails; change the return type so title is optional (title?: string),
restrict the DynamoDB query to only the needed attributes using
ProjectionExpression (e.g., username, name, title) and ExpressionAttributeNames
to reduce payload, and make the per-org catch block resilient by logging the
error and returning an empty array for that org instead of throwing (still
rethrow BaseError). Ensure callers/platform types are updated to accept title as
possibly undefined.

import { AllOrganizationNameList, getOrgByName } from "@acm-uiuc/js-shared";
import rateLimiter from "api/plugins/rateLimiter.js";
import { withRoles, withTags } from "api/components/index.js";
import { z } from "zod/v4";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify Zod v4 compatibility across the codebase.

The import has been updated to use zod/v4, which introduces breaking changes. While the usage in this file appears compatible, ensure that other files using Zod schemas have been reviewed for:

  • Default value handling changes (.default() now short-circuits on undefined)
  • Number validation changes (no infinite values, .int() only allows safe integers)
  • Record schema changes (now requires both key and value schemas)
  • Function schema redesign
  • Enum handling changes

🏁 Script executed:

#!/bin/bash
# Description: Search for Zod patterns that may be affected by v4 breaking changes

echo "=== Checking for .default() usage with transforms ==="
rg -n "\.transform\(" --type=ts -A 5 | rg "\.default\("

echo -e "\n=== Checking for z.record() without explicit schemas ==="
rg -n "z\.record\([^,)]*\)" --type=ts

echo -e "\n=== Checking for z.function() usage ==="
rg -n "z\.function\(" --type=ts

echo -e "\n=== Checking for number validation with potential infinity ==="
rg -n "z\.number\(\)" --type=ts -A 2 | rg -i "infinite|infinity"

Length of output: 309


🏁 Script executed:

#!/bin/bash
# Get the full line from events.ts for the z.record() call
echo "=== Full z.record() line from events.ts ==="
sed -n '8p' src/common/types/events.ts

echo -e "\n=== Verify z.record() context (lines 5-12) ==="
sed -n '5,12p' src/common/types/events.ts

echo -e "\n=== Check all Zod version imports in the codebase ==="
rg -n "from\s+[\"']zod" --type=ts

echo -e "\n=== Check for .merge() chains or other schema composition ==="
rg -n "\.merge\(" --type=ts | head -20

echo -e "\n=== Check for .pick() or .omit() usage ==="
rg -n "\.pick\(|\.omit\(" --type=ts | head -20

Length of output: 3811



🏁 Script executed:

#!/bin/bash
# Check if api/functions/organizations.ts is related to the reviewed file
echo "=== Check what's imported from api/functions/organizations.ts ==="
rg -n "from.*api/functions/organizations" --type=ts

echo -e "\n=== Check what api/functions/organizations.ts exports ==="
head -50 src/api/functions/organizations.ts | grep -E "export|^import"

echo -e "\n=== Check if api/functions/organizations.ts was modified in this PR ==="
rg -n "import.*organizations" src/api/routes/organizations.ts

echo -e "\n=== Verify Zod import resolution - check if 'zod' resolves to v4 ==="
rg -n "\"zod\"[^/]" --type=ts | head -10

Length of output: 1825


Update src/api/functions/organizations.ts to use Zod v4.

The reviewed file imports from api/functions/organizations.ts, which still uses import z from "zod" instead of import z from "zod/v4". This creates a Zod version mismatch that can cause validation and type compatibility issues. Update line 20 of src/api/functions/organizations.ts to:

import z from "zod/v4";

Additionally, verify all other files importing from this module are compatible with the updated import.

🤖 Prompt for AI Agents
In src/api/functions/organizations.ts around line 20, the file currently imports
Zod using the old path; change the import to use Zod v4 by replacing the
existing import with: import z from "zod/v4"; then run a quick grep across the
repo for imports of this module and ensure callers remain compatible with the v4
export (update any imports or type usages if necessary) and run the test/compile
step to confirm no type or runtime errors.

export { sendSaleEmailHandler } from "./sendSaleEmailHandler.js";
export { emailNotificationsHandler } from "./emailNotifications.js";
export { createOrgGithubTeamHandler } from "./createOrgGithubTeam.js";
export { syncExecCouncilHandler } from "./syncExecCouncil.js";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

ESLint import/extensions warning.

If using TS + ESM with NodeNext and emitted .js paths, keep the extension and relax the rule; otherwise drop the extension for local exports.


🏁 Script executed:

#!/bin/bash
# Show existing pattern in this repo
rg -nP '^export\s+\{.*\}\s+from\s+"\.\/.*\.js";' src/api/sqs/handlers | wc -l

Length of output: 1



---

🏁 Script executed:

```shell
#!/bin/bash
# Check tsconfig.json for module resolution strategy
cat tsconfig.json 2>/dev/null | grep -A5 -B5 "moduleResolution\|module"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check for ESLint configuration files and import/extensions rule
find . -maxdepth 2 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) 2>/dev/null | head -5

Length of output: 19


🏁 Script executed:

#!/bin/bash
# Check package.json for ESLint config and import/extensions rule mention
cat package.json 2>/dev/null | grep -A20 '"eslintConfig"' | head -30

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Show actual exports with .js in src/api/sqs/handlers to confirm pattern
rg -nP '^export\s+\{.*\}\s+from\s+"\.\/.*\.js";' src/api/sqs/handlers

Length of output: 680



---

🏁 Script executed:

```shell
#!/bin/bash
# Read eslint.config.mjs to see import/extensions rule
cat eslint.config.mjs | head -100

Length of output: 2565


🏁 Script executed:

#!/bin/bash
# Read full tsconfig.json
cat tsconfig.json

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check if .js extensions are used elsewhere in codebase (not just handlers)
rg -nP '^export\s+\{.*\}\s+from\s+"\.\/.*\.js";' --type=ts --max-count=20 | head -20

Length of output: 680


Remove .js extensions from all export statements—they violate the ESLint rule.

The project's ESLint configuration explicitly forbids .js extensions on local imports and exports ("import/extensions": ["error", "ignorePackages", { js: "never", ... }] at eslint.config.mjs). All 7 exports at lines 1–7 must drop the .js extension to comply:

export { syncExecCouncilHandler } from "./syncExecCouncil";
🧰 Tools
🪛 ESLint

[error] 7-7: Unexpected use of file extension "js" for "./syncExecCouncil.js"

(import/extensions)

🤖 Prompt for AI Agents
In src/api/sqs/handlers/index.ts around line 7, the export statement includes a
.js extension which violates the project's ESLint import/extensions rule; remove
the .js extension from this and the other local export statements so they read
e.g. export { syncExecCouncilHandler } from "./syncExecCouncil" and repeat for
all seven exports to comply with the lint rule.

Comment on lines +58 to +91
// Convert to set of usernames (without @illinois.edu)
const votingLeadUsernames = new Set(
votingLeads.map((lead) => lead.username),
);

logger.info(
`Found ${votingLeadUsernames.size} voting leads across all organizations.`,
);

// Get current exec council members from Entra ID
const currentMembers = await listGroupMembers(
entraIdToken,
execCouncilGroup,
);

// Convert to set of emails
const currentMemberEmails = new Set(
currentMembers
.map((member) => member.email)
.filter((email) => email && email.endsWith("@illinois.edu")),
);

logger.info(
`Current exec council has ${currentMemberEmails.size} members from @illinois.edu domain.`,
);

// Determine who to add and who to remove
const toAdd = Array.from(votingLeadUsernames).filter(
(username) => !currentMemberEmails.has(username),
);
const toRemove = Array.from(currentMemberEmails).filter(
(email) => !votingLeadUsernames.has(email),
);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Normalize identifiers: comparing usernames to emails will produce incorrect add/remove sets.

You build a Set of usernames then compare against email addresses, leading to false diffs and unintended membership churn.

-    // Convert to set of usernames (without @illinois.edu)
-    const votingLeadUsernames = new Set(
-      votingLeads.map((lead) => lead.username),
-    );
+    // Normalize voting leads to lowercased @illinois.edu emails
+    const EMAIL_DOMAIN = "@illinois.edu";
+    const votingLeadEmails = new Set(
+      votingLeads
+        .map((lead) => lead.username?.toLowerCase())
+        .filter((u): u is string => !!u && u.length > 0)
+        .map((u) => (u.includes("@") ? u : `${u}${EMAIL_DOMAIN}`))
+    );
@@
-    const currentMemberEmails = new Set(
-      currentMembers
-        .map((member) => member.email)
-        .filter((email) => email && email.endsWith("@illinois.edu")),
-    );
+    const currentMemberEmails = new Set(
+      currentMembers
+        .map((member) => member.email?.toLowerCase())
+        .filter((email): email is string => !!email && email.endsWith("@illinois.edu")),
+    );
@@
-    const toAdd = Array.from(votingLeadUsernames).filter(
-      (username) => !currentMemberEmails.has(username),
-    );
-    const toRemove = Array.from(currentMemberEmails).filter(
-      (email) => !votingLeadUsernames.has(email),
-    );
+    const toAdd = Array.from(votingLeadEmails).filter(
+      (email) => !currentMemberEmails.has(email),
+    );
+    const toRemove = Array.from(currentMemberEmails).filter(
+      (email) => !votingLeadEmails.has(email),
+    );
@@
-    for (const username of toAdd) {
+    for (const email of toAdd) {
       try {
-        logger.info(`Adding ${username} to exec council...`);
+        logger.info(`Adding ${email} to exec council...`);
         await modifyGroup(
           entraIdToken,
-          username,
+          email,
           execCouncilGroup,
           EntraGroupActions.ADD,
           dynamo,
         );
-        logger.info(`Successfully added ${username} to exec council.`);
+        logger.info(`Successfully added ${email} to exec council.`);
       } catch (error) {
         logger.error(
           error,
-          `Failed to add ${username} to exec council. Continuing with other members...`,
+          `Failed to add ${email} to exec council. Continuing with other members...`,
         );
       }
     }

🏁 Script executed:

#!/bin/bash
# Inspect getAllVotingLeads return shape/usages to confirm username semantics
rg -nP --type=ts -C2 '\bgetAllVotingLeads\s*\('
rg -nP --type=ts -C2 '\busername\b' api/functions | head -n 50

Length of output: 731


I need to gather more information about the return type of getAllVotingLeads and the modifyGroup function signature to verify the review comment's claim. Let me inspect those now.


---

🏁 Script executed:

```shell
#!/bin/bash
# Get the full implementation of getAllVotingLeads to see return type
rg -nP --type=ts -A30 'export async function getAllVotingLeads' src/api/functions/organizations.ts

# Get modifyGroup function signature and implementation
rg -nP --type=ts -A10 'function modifyGroup|export.*modifyGroup' src/

Length of output: 1857


Now I need to see the complete implementation in syncExecCouncil.ts to verify how usernames are being passed to modifyGroup. Let me check the full code around the add/remove loops:


---

🏁 Script executed:

```shell
#!/bin/bash
# Get the complete implementation of the sync handler, particularly the toAdd/toRemove loops
rg -nP --type=ts -A20 'for \(const username of toAdd\)|for \(const.*of toAdd\)' src/api/sqs/handlers/syncExecCouncil.ts

Length of output: 686


Normalize identifiers: comparing usernames to emails will produce incorrect add/remove sets.

You build a Set of usernames then compare against email addresses, leading to false diffs and unintended membership churn. The modifyGroup function expects an email (line 262 of src/api/functions/entraId.ts) and validates it ends with @illinois.edu (line 268), but the code passes raw usernames without domain.

-    // Convert to set of usernames (without @illinois.edu)
-    const votingLeadUsernames = new Set(
-      votingLeads.map((lead) => lead.username),
-    );
+    // Normalize voting leads to lowercased @illinois.edu emails
+    const EMAIL_DOMAIN = "@illinois.edu";
+    const votingLeadEmails = new Set(
+      votingLeads
+        .map((lead) => lead.username?.toLowerCase())
+        .filter((u): u is string => !!u && u.length > 0)
+        .map((u) => (u.includes("@") ? u : `${u}${EMAIL_DOMAIN}`))
+    );
@@
-    const currentMemberEmails = new Set(
-      currentMembers
-        .map((member) => member.email)
-        .filter((email) => email && email.endsWith("@illinois.edu")),
-    );
+    const currentMemberEmails = new Set(
+      currentMembers
+        .map((member) => member.email?.toLowerCase())
+        .filter((email): email is string => !!email && email.endsWith("@illinois.edu")),
+    );
@@
-    const toAdd = Array.from(votingLeadUsernames).filter(
-      (username) => !currentMemberEmails.has(username),
-    );
-    const toRemove = Array.from(currentMemberEmails).filter(
-      (email) => !votingLeadUsernames.has(email),
-    );
+    const toAdd = Array.from(votingLeadEmails).filter(
+      (email) => !currentMemberEmails.has(email),
+    );
+    const toRemove = Array.from(currentMemberEmails).filter(
+      (email) => !votingLeadEmails.has(email),
+    );
@@
-    for (const username of toAdd) {
+    for (const email of toAdd) {
       try {
-        logger.info(`Adding ${username} to exec council...`);
+        logger.info(`Adding ${email} to exec council...`);
         await modifyGroup(
           entraIdToken,
-          username,
+          email,
           execCouncilGroup,
           EntraGroupActions.ADD,
           dynamo,
         );
-        logger.info(`Successfully added ${username} to exec council.`);
+        logger.info(`Successfully added ${email} to exec council.`);
       } catch (error) {
         logger.error(
           error,
-          `Failed to add ${username} to exec council. Continuing with other members...`,
+          `Failed to add ${email} to exec council. Continuing with other members...`,
         );
       }
     }
🤖 Prompt for AI Agents
In src/api/sqs/handlers/syncExecCouncil.ts around lines 58 to 91, the code
builds a Set of bare usernames and compares it to a Set of @illinois.edu emails
which produces incorrect add/remove lists; change the normalization to use the
same identifier for both sides (emails): convert votingLeads' usernames into
full emails (append @illinois.edu, validate/trim/lowercase the username first)
before creating votingLeadEmails Set, ensure currentMemberEmails are lowercased
and filtered for the @illinois.edu domain, then compute toAdd and toRemove by
comparing these email Sets so the values passed to modifyGroup are valid
@illinois.edu addresses.

Comment on lines +81 to 84
),
[AvailableSQSFunctions.SyncExecCouncil]: createSQSSchema(
AvailableSQSFunctions.SyncExecCouncil, z.object({})
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Prettier violations and add trailing comma in union.

Tidy formatting to satisfy prettier/prettier.

   [AvailableSQSFunctions.CreateOrgGithubTeam]: createSQSSchema(
     AvailableSQSFunctions.CreateOrgGithubTeam, z.object({
       orgName: z.enum(AllOrganizationNameList),
       githubTeamName: z.string().min(1),
       githubTeamDescription: z.string().min(1)
     })
-  ),
-  [AvailableSQSFunctions.SyncExecCouncil]: createSQSSchema(
-    AvailableSQSFunctions.SyncExecCouncil, z.object({})
-  )
+  ),
+  [AvailableSQSFunctions.SyncExecCouncil]: createSQSSchema(
+    AvailableSQSFunctions.SyncExecCouncil,
+    z.object({}),
+  ),
 } as const;
 ...
 export const sqsPayloadSchema = z.discriminatedUnion("function", [
   sqsPayloadSchemas[AvailableSQSFunctions.Ping],
   sqsPayloadSchemas[AvailableSQSFunctions.EmailMembershipPass],
   sqsPayloadSchemas[AvailableSQSFunctions.ProvisionNewMember],
   sqsPayloadSchemas[AvailableSQSFunctions.SendSaleEmail],
   sqsPayloadSchemas[AvailableSQSFunctions.EmailNotifications],
-  sqsPayloadSchemas[AvailableSQSFunctions.CreateOrgGithubTeam],
-  sqsPayloadSchemas[AvailableSQSFunctions.SyncExecCouncil]
+  sqsPayloadSchemas[AvailableSQSFunctions.CreateOrgGithubTeam],
+  sqsPayloadSchemas[AvailableSQSFunctions.SyncExecCouncil],
 ] as
   const);

Also applies to: 101-103

🧰 Tools
🪛 ESLint

[error] 83-83: Replace ·z.object({}) with ⏎····z.object({}),

(prettier/prettier)


[error] 84-84: Insert ,

(prettier/prettier)

🤖 Prompt for AI Agents
In src/common/types/sqsMessage.ts around lines 81-84 (and also apply same fix to
101-103), the block defining the schema has Prettier violations and is missing a
trailing comma in the union; reformat the multi-line createSQSSchema call to
satisfy Prettier (proper indentation, line breaks) and add a trailing comma
after the last array entry/argument so the union and object literal end with a
comma; ensure the same formatting and trailing-comma fix is applied to lines
101-103 as well.

Copy link
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: 1

♻️ Duplicate comments (1)
src/api/sqs/handlers/syncExecCouncil.ts (1)

88-121: Identifier normalization issue persists from previous review.

The username-to-email comparison bug identified in the previous review remains unaddressed. Bare usernames are compared against @illinois.edu emails, causing incorrect add/remove operations and failures when passing usernames to modifyGroup (which expects emails).

Please apply the normalization fix proposed in the previous review comment.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8c268b8 and 2b4672f.

📒 Files selected for processing (1)
  • src/api/sqs/handlers/syncExecCouncil.ts (1 hunks)
🧰 Additional context used
🪛 ESLint
src/api/sqs/handlers/syncExecCouncil.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/WDDlCXpPkc'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 1-1: Unexpected use of file extension "js" for "common/types/sqsMessage.js"

(import/extensions)


[error] 6-6: Unexpected use of file extension "js" for "../index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/organizations.js"

(import/extensions)


[error] 19-19: Unexpected use of file extension "js" for "api/functions/entraId.js"

(import/extensions)


[error] 21-21: Unexpected use of file extension "js" for "common/types/iam.js"

(import/extensions)


[error] 22-22: Unexpected use of file extension "js" for "api/functions/sts.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Unit Tests
  • GitHub Check: Build Application
🔇 Additional comments (1)
src/api/sqs/handlers/syncExecCouncil.ts (1)

127-144: Good error handling for individual member additions.

The per-member try-catch pattern allows the sync to continue even if individual additions fail, which is appropriate for bulk operations.

Copy link
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

🧹 Nitpick comments (1)
src/ui/pages/organization/ManageOrganizationForm.tsx (1)

573-573: Good clarity improvement with organizationId interpolation.

The interpolation makes it explicit which organization's voting rights are being discussed, aligning well with the PR's objective to separate exec membership from org admin privileges.

If organizationId is a technical identifier (UUID or hash) rather than a human-readable slug, consider using an organization display name for better UX. However, for admin interfaces, showing the org ID is often appropriate and helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4672f and 1fe80d0.

📒 Files selected for processing (1)
  • src/ui/pages/organization/ManageOrganizationForm.tsx (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Application
  • GitHub Check: Run Unit Tests
🔇 Additional comments (1)
src/ui/pages/organization/ManageOrganizationForm.tsx (1)

590-591: LGTM! Clear organization-specific messaging.

The updated alert text properly clarifies that voting rights are organization-specific, consistent with the checkbox description change on line 573. The wording is clear and aligns well with the PR's objective.

Copy link
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

♻️ Duplicate comments (1)
src/api/functions/organizations.ts (1)

539-585: Address the unresolved issues from previous review.

The past review comment on this function remains unaddressed:

  1. Type safety: title is declared as string but can be undefined. Line 546 should declare title?: string and line 568 should not cast to string.
  2. Payload optimization: Add ProjectionExpression to fetch only needed attributes (username, name, title, nonVotingMember) rather than full items.
  3. Resiliency: Promise.all on line 583 fails the entire operation if any single org query fails. Consider using Promise.allSettled to tolerate partial failures.

Apply this diff to address all three issues:

 export async function getAllVotingLeads({
   dynamoClient,
   logger,
 }: {
   dynamoClient: DynamoDBClient;
   logger: ValidLoggers;
 }): Promise<
-  Array<{ username: string; org: string; name: string; title: string }>
+  Array<{ username: string; org: string; name: string; title?: string }>
 > {
   // Query all organizations in parallel for better performance
   const queryPromises = AllOrganizationNameList.map(async (orgName) => {
     const leadsQuery = new QueryCommand({
       TableName: genericConfig.SigInfoTableName,
       KeyConditionExpression: "primaryKey = :leadName",
       ExpressionAttributeValues: {
         ":leadName": { S: `LEAD#${orgName}` },
       },
+      ProjectionExpression: "#u, #n, title, nonVotingMember",
+      ExpressionAttributeNames: { "#u": "username", "#n": "name" },
       ConsistentRead: true,
     });
 
     try {
       const responseMarshall = await dynamoClient.send(leadsQuery);
       if (responseMarshall.Items) {
         return responseMarshall.Items.map((x) => unmarshall(x))
           .filter((x) => x.username && !x.nonVotingMember)
           .map((x) => ({
             username: x.username as string,
             org: orgName,
             name: x.name as string,
-            title: x.title as string,
+            title: x.title,
           }));
       }
       return [];
     } catch (e) {
       if (e instanceof BaseError) {
         throw e;
       }
       logger.error(e);
-      throw new DatabaseFetchError({
-        message: `Failed to get leads for org ${orgName}.`,
-      });
+      // Log error but continue with other orgs for resiliency
+      logger.error(`Failed to get leads for org ${orgName}, continuing with other orgs`);
+      return [];
     }
   });
 
   const results = await Promise.all(queryPromises);
   return results.flat();
 }

Note: The resiliency change catches errors per-org and returns an empty array, allowing other orgs to succeed. If you prefer Promise.allSettled as suggested in the past review, that's also valid.

🧹 Nitpick comments (1)
src/api/functions/organizations.ts (1)

587-636: Consider adding ProjectionExpression to reduce payload.

The sequential query pattern with early return is appropriate for an existence check. However, you can optimize payload size by fetching only the nonVotingMember attribute.

Apply this diff:

     const leadsQuery = new QueryCommand({
       TableName: genericConfig.SigInfoTableName,
       KeyConditionExpression: "primaryKey = :leadName AND entryId = :username",
       ExpressionAttributeValues: {
         ":leadName": { S: `LEAD#${orgName}` },
         ":username": { S: username },
       },
+      ProjectionExpression: "nonVotingMember",
       ConsistentRead: true,
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 1fe80d0 and 0a12700.

📒 Files selected for processing (1)
  • src/api/functions/organizations.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Unit Tests
  • GitHub Check: Build Application
🔇 Additional comments (1)
src/api/functions/organizations.ts (1)

104-110: LGTM: Clean addition of nonVotingMember field.

The implementation correctly defaults nonVotingMember to false when undefined and includes explicit type annotations.

@devksingh4 devksingh4 merged commit c697304 into main Oct 26, 2025
6 of 9 checks passed
@devksingh4 devksingh4 deleted the dsingh14/seperate-exec-from-org-admin branch October 26, 2025 01:26
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.

2 participants