fix(insights): convert string filter values to numbers for integer columns (#24730)

## What does this PR do?

Fixes PostgreSQL error "operator does not exist: integer = text" when filtering
by `bookingStatusOrder` and `bookingUserId` fields. Frontend sends filter values as
strings, but these database columns are integers requiring type conversion.

Changes:
- Convert `bookingStatusOrder` filter values from strings to numbers
- Convert `bookingUserId` filter values from strings to numbers
- Update tests to use numeric values for `bookingStatusOrder` filters
- Add test for string-to-number conversion in `bookingUserId` filter
- Fix `Prisma.Sql` composition in 3 integration tests

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] N/A - I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.


<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Converts string filter values to numbers for integer columns in Insights routing to prevent PostgreSQL type errors. Fixes filtering by bookingStatusOrder and bookingUserId when the frontend sends strings.

- **Bug Fixes**
  - Cast bookingStatusOrder and bookingUserId filter values to numbers and use = ANY([...]).
  - Update tests to expect numeric arrays; add test for string-to-number conversion on bookingUserId.
  - Correct Prisma.sql composition in three integration tests before calling $queryRaw.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Eunjae Lee
2025-10-28 10:34:56 +00:00
committed by GitHub
parent b620581d7a
commit c7c8564b63
2 changed files with 68 additions and 16 deletions
@@ -710,7 +710,12 @@ export class InsightsRoutingBaseService {
// Extract booking status order filter
const bookingStatusOrder = filtersMap["bookingStatusOrder"];
if (bookingStatusOrder && isMultiSelectFilterValue(bookingStatusOrder.value)) {
const statusCondition = makeSqlCondition(bookingStatusOrder.value);
// Convert string values to numbers for integer column
const integerFilterValue = {
...bookingStatusOrder.value,
data: bookingStatusOrder.value.data.map((order) => Number(order)),
};
const statusCondition = makeSqlCondition(integerFilterValue);
if (statusCondition) {
conditions.push(Prisma.sql`rfrd."bookingStatusOrder" ${statusCondition}`);
}
@@ -764,7 +769,15 @@ export class InsightsRoutingBaseService {
// Extract member user IDs filter (multi-select)
const memberUserIds = filtersMap["bookingUserId"];
if (memberUserIds && isMultiSelectFilterValue(memberUserIds.value)) {
conditions.push(Prisma.sql`rfrd."bookingUserId" = ANY(${memberUserIds.value.data})`);
// Convert string values to numbers for integer column
const integerFilterValue = {
...memberUserIds.value,
data: memberUserIds.value.data.map((id) => Number(id)),
};
const userIdCondition = makeSqlCondition(integerFilterValue);
if (userIdCondition) {
conditions.push(Prisma.sql`rfrd."bookingUserId" ${userIdCondition}`);
}
}
// Extract form ID filter (single-select)
@@ -759,9 +759,10 @@ describe("InsightsRoutingService Integration Tests", () => {
});
const baseConditions = await service.getBaseConditions();
const results = await prisma.$queryRaw<Array<{ id: number }>>`
const query = Prisma.sql`
SELECT id FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions}
`;
const results = await prisma.$queryRaw<Array<{ id: number }>>(query);
// Should only return the authorized user's form response
expect(results).toHaveLength(2);
@@ -863,9 +864,10 @@ describe("InsightsRoutingService Integration Tests", () => {
});
const baseConditions = await service.getBaseConditions();
const results = await prisma.$queryRaw<Array<{ id: number }>>`
const query = Prisma.sql`
SELECT id FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions}
`;
const results = await prisma.$queryRaw<Array<{ id: number }>>(query);
// Should only return the authorized user's form response
expect(results).toHaveLength(1);
@@ -953,9 +955,10 @@ describe("InsightsRoutingService Integration Tests", () => {
});
const baseConditions = await service.getBaseConditions();
const results = await prisma.$queryRaw<Array<{ id: number }>>`
const query = Prisma.sql`
SELECT id FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions}
`;
const results = await prisma.$queryRaw<Array<{ id: number }>>(query);
// Should return both form responses (original user's and team member's)
expect(results).toHaveLength(2);
@@ -1070,7 +1073,7 @@ describe("InsightsRoutingService Integration Tests", () => {
id: "bookingStatusOrder",
value: {
type: ColumnFilterType.MULTI_SELECT,
data: ["pending", "accepted"],
data: ["2", "1"], // String values that will be converted to numbers
},
},
],
@@ -1081,7 +1084,7 @@ describe("InsightsRoutingService Integration Tests", () => {
expect(filterConditions).toEqual(
Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${
defaultFilters.endDate
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${["pending", "accepted"]}))`
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[2, 1]}))`
);
await testData.cleanup();
@@ -1204,6 +1207,45 @@ describe("InsightsRoutingService Integration Tests", () => {
await testData.cleanup();
});
it("should filter by member user IDs with string values (multi-select)", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
orgRole: MembershipRole.OWNER,
});
const defaultFilters = createDefaultFilters();
const service = new InsightsRoutingService({
prisma,
options: {
scope: "user",
userId: testData.user.id,
orgId: testData.org.id,
teamId: undefined,
},
filters: {
...defaultFilters,
columnFilters: [
{
id: "bookingUserId",
value: {
type: ColumnFilterType.MULTI_SELECT,
data: [String(testData.user.id), "999"], // String values that will be converted to numbers
},
},
],
},
});
const filterConditions = await service.getFilterConditions();
expect(filterConditions).toEqual(
Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${
defaultFilters.endDate
}::timestamp) AND (rfrd."bookingUserId" = ANY(${[testData.user.id, 999]}))`
);
await testData.cleanup();
});
it("should filter by attendee name (text)", async () => {
const testData = await createTestData({
teamRole: MembershipRole.OWNER,
@@ -1450,7 +1492,7 @@ describe("InsightsRoutingService Integration Tests", () => {
id: "bookingStatusOrder",
value: {
type: ColumnFilterType.MULTI_SELECT,
data: ["pending"],
data: ["2"], // String value that will be converted to number (2 = PENDING)
},
},
{
@@ -1476,7 +1518,7 @@ describe("InsightsRoutingService Integration Tests", () => {
Prisma.sql`(((rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${
defaultFilters.endDate
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[
"pending",
2,
]}))) AND (rfrd."bookingAssignmentReason" ILIKE ${`%manual%`})) AND (EXISTS (
SELECT 1 FROM "RoutingFormResponseField" rrf
WHERE rrf."responseId" = rfrd."id"
@@ -1556,7 +1598,7 @@ describe("InsightsRoutingService Integration Tests", () => {
id: "bookingStatusOrder",
value: {
type: ColumnFilterType.MULTI_SELECT,
data: ["pending", "accepted"],
data: ["2", "1"], // String values that will be converted to numbers (2=PENDING, 1=ACCEPTED)
},
},
],
@@ -1567,10 +1609,7 @@ describe("InsightsRoutingService Integration Tests", () => {
expect(filterConditions).toEqual(
Prisma.sql`((rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${
defaultFilters.endDate
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[
"pending",
"accepted",
]}))) AND (rfrd."formId" = ${"form-456"})`
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[2, 1]}))) AND (rfrd."formId" = ${"form-456"})`
);
await testData.cleanup();
@@ -1600,7 +1639,7 @@ describe("InsightsRoutingService Integration Tests", () => {
id: "bookingStatusOrder", // System filter
value: {
type: ColumnFilterType.MULTI_SELECT,
data: ["pending"],
data: ["2"], // String value that will be converted to number (2 = PENDING)
},
},
{
@@ -1618,7 +1657,7 @@ describe("InsightsRoutingService Integration Tests", () => {
expect(filterConditions).toEqual(
Prisma.sql`((rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${
defaultFilters.endDate
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${["pending"]}))) AND (EXISTS (
}::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[2]}))) AND (EXISTS (
SELECT 1 FROM "RoutingFormResponseField" rrf
WHERE rrf."responseId" = rfrd."id"
AND rrf."fieldId" = ${customFieldId}