diff --git a/packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts b/packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts deleted file mode 100644 index c70f971ef0..0000000000 --- a/packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { describe, expect, it, afterEach, vi } from "vitest"; - -import { jsonLogicToPrisma } from "../jsonLogicToPrisma"; - -afterEach(() => { - vi.resetAllMocks(); -}); - -describe("jsonLogicToPrisma(Reporting)", () => { - describe("Text Operand", () => { - it("should support 'Equals' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "A"] }] }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "A", - }, - }, - ], - }); - }); - - it("should support 'Not Equals' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ "!=": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abc"] }] }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - NOT: { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "abc", - }, - }, - }, - ], - }); - }); - - it("should support 'Contains' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ in: ["A", { var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }] }] }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - string_contains: "A", - }, - }, - ], - }); - }); - - it("should support 'Not Contains' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ "!": { in: ["a", { var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }] } }] }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - NOT: { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - string_contains: "a", - }, - }, - }, - ], - }); - }); - - describe("Number Type", () => { - it("should support 'greater than' operator", () => { - let prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { - ">": [ - { - var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f", - }, - // Giving a string here to test that it is converted to a number - "100", - ], - }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gt: 100 } }], - }); - - prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { - ">": [ - { - var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f", - }, - // A number would also work - 100, - ], - }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gt: 100 } }], - }); - }); - it("should support 'greater than or equal to' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { - ">=": [ - { - var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f", - }, - // Giving a string here to test that it is converted to a number - "100", - ], - }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gte: 100 } }], - }); - }); - it("should support 'less than' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { - "<": [ - { - var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f", - }, - // Giving a string here to test that it is converted to a number - "100", - ], - }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], lt: 100 } }], - }); - }); - it("should support 'less than or equal to' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { - "<=": [ - { - var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f", - }, - // Giving a string here to test that it is converted to a number - "100", - ], - }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], lte: 100 } }], - }); - }); - it("'Equals' operator should query with string as well as number", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "1"] }] }, - }); - - expect(prismaWhere).toEqual({ - AND: [ - { - OR: [ - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "1", - }, - }, - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: 1, - }, - }, - ], - }, - ], - }); - }); - }); - }); - - describe("MultiSelect", () => { - it("should support 'Equals' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { all: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, { in: [{ var: "" }, ["C", "D"]] }] }, - ], - }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - response: { path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], array_contains: ["C", "D"] }, - }, - ], - }); - }); - - it("should support 'Any In' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ in: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, ["C", "D"]] }] }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - OR: [ - { - response: { - path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], - equals: "C", - }, - }, - { - response: { - path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], - equals: "D", - }, - }, - ], - }, - ], - }); - }); - - it("should support 'Not In' operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { and: [{ "!": { in: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, ["C", "D"]] } }] }, - }); - expect(prismaWhere).toEqual({ - AND: [ - { - NOT: { - OR: [ - { - response: { - path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], - equals: "C", - }, - }, - { - response: { - path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], - equals: "D", - }, - }, - ], - }, - }, - ], - }); - }); - - it("should support where All Match ['Equals', 'Equals'] operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - and: [ - { "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "a"] }, - { "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "b"] }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - AND: [ - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "a", - }, - }, - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "b", - }, - }, - ], - }); - }); - - it("should support where Any Match ['Equals', 'Equals'] operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - or: [ - { "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "a"] }, - { "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "b"] }, - ], - }, - }); - - expect(prismaWhere).toEqual({ - OR: [ - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "a", - }, - }, - { - response: { - path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], - equals: "b", - }, - }, - ], - }); - }); - - it("should support where None Match ['Equals', 'Equals'] operator", () => { - const prismaWhere = jsonLogicToPrisma({ - logic: { - "!": { - or: [ - { "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abc"] }, - { "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abcd"] }, - ], - }, - }, - }); - - expect(prismaWhere).toEqual({ - NOT: { - OR: [ - { response: { path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], equals: "abc" } }, - { response: { path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], equals: "abcd" } }, - ], - }, - }); - }); - }); -}); diff --git a/packages/app-store/routing-forms/jsonLogicToPrisma.ts b/packages/app-store/routing-forms/jsonLogicToPrisma.ts deleted file mode 100644 index 9c19f4b95a..0000000000 --- a/packages/app-store/routing-forms/jsonLogicToPrisma.ts +++ /dev/null @@ -1,276 +0,0 @@ -// It can have many shapes, so just use any and we rely on unit tests to test all those scenarios. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type LogicData = Partial>; -type NegatedLogicData = { - "!": LogicData; -}; - -export type JsonLogicQuery = { - logic: { - and?: LogicData[]; - or?: LogicData[]; - "!"?: { - and?: LogicData[]; - or?: LogicData[]; - }; - } | null; -}; - -type PrismaWhere = { - AND?: ReturnType[]; - OR?: ReturnType[]; - NOT?: PrismaWhere; -}; - -const OPERATOR_MAP = { - "==": { - operator: "equals", - secondaryOperand: null, - }, - in: { - operator: "string_contains", - secondaryOperand: null, - }, - "!=": { - operator: "NOT.equals", - secondaryOperand: null, - }, - "!": { - operator: "equals", - secondaryOperand: "", - }, - "!!": { - operator: "NOT.equals", - secondaryOperand: "", - }, - ">": { - operator: "gt", - secondaryOperand: null, - }, - ">=": { - operator: "gte", - secondaryOperand: null, - }, - "<": { - operator: "lt", - secondaryOperand: null, - }, - "<=": { - operator: "lte", - secondaryOperand: null, - }, - all: { - operator: "array_contains", - secondaryOperand: null, - }, -}; - -/** - * Operators supported on array of basic queries - */ -const GROUP_OPERATOR_MAP = { - and: "AND", - or: "OR", - "!": "NOT", -} as const; - -const NumberOperators = [">", ">=", "<", "<="]; - -const negatePrismaWhereClauseIfNeeded = (data: any, isNegation: boolean) => { - if (!isNegation) { - return data; - } - return { - NOT: { - ...data, - }, - }; -}; - -const convertSingleQueryToPrismaWhereClause = ( - operatorName: keyof typeof OPERATOR_MAP, - logicData: LogicData, - isNegation: boolean -) => { - const mappedOperator = OPERATOR_MAP[operatorName].operator; - const staticSecondaryOperand = OPERATOR_MAP[operatorName].secondaryOperand; - isNegation = isNegation || mappedOperator.startsWith("NOT."); - const prismaOperator = mappedOperator.replace("NOT.", ""); - const operands = - logicData[operatorName] instanceof Array ? logicData[operatorName] : [logicData[operatorName]]; - - if (operatorName === "in" && operands[0]?.var && Array.isArray(operands[1])) { - // Case A: Item "in" array - // operands[0]: { var: ... } - // operands[1]: items to test against - - // Convert 'in' operator to union of OR clauses - return negatePrismaWhereClauseIfNeeded( - { - OR: (operands[1] ?? []).map((value) => ({ - response: { - path: [operands[0].var, "value"], - [`${OPERATOR_MAP["=="].operator}`]: value, - }, - })), - }, - isNegation - ); - } - - if (operatorName === "in" && typeof operands[0] === "string" && operands[1]?.var) { - // Case B: String "in" string - // operands[0]: string to test against - // operands[1]: { var: ... } - - return negatePrismaWhereClauseIfNeeded( - { - response: { - path: [operands[1].var, "value"], - [`${prismaOperator}`]: operands[0], - }, - }, - isNegation - ); - } - - const mainOperand = operands[0].var; - let secondaryOperand; - - if (operatorName === "all") { - secondaryOperand = operands[1].in[1]; - } else { - secondaryOperand = staticSecondaryOperand || operands[1] || ""; - } - - const isNumberOperator = NumberOperators.includes(operatorName); - const secondaryOperandAsNumber = typeof secondaryOperand === "string" ? Number(secondaryOperand) : null; - - if (secondaryOperandAsNumber && isNumberOperator) { - // We know that it's number operator so Prisma should query number - // Note that if we get string values in DB(e.g. '100'), those values can't be filtered with number operators. - return negatePrismaWhereClauseIfNeeded( - { - response: { - path: [mainOperand, "value"], - [`${prismaOperator}`]: secondaryOperandAsNumber, - }, - }, - isNegation - ); - } - - if (secondaryOperandAsNumber && !isNumberOperator) { - // We know that it's not number operator but the input field might have been a number and thus stored value in DB as number. - // Also, even for input type=number we might accidentally get string value(e.g. '100'). So, let reporting do it's best job with both number and string. - return negatePrismaWhereClauseIfNeeded( - { - OR: [ - { - response: { - path: [mainOperand, "value"], - // Query as string e.g. equals '100' - [`${prismaOperator}`]: secondaryOperand, - }, - }, - { - response: { - path: [mainOperand, "value"], - // Query as number e.g. equals 100 - [`${prismaOperator}`]: secondaryOperandAsNumber, - }, - }, - ], - }, - isNegation - ); - } - - return negatePrismaWhereClauseIfNeeded( - { - response: { - path: [mainOperand, "value"], - [`${prismaOperator}`]: secondaryOperand, - }, - }, - isNegation - ); -}; - -const isNegation = (logicData: LogicData | NegatedLogicData) => { - if ("!" in logicData) { - const negatedLogicData = logicData["!"]; - - for (const [operatorName] of Object.entries(OPERATOR_MAP)) { - if (negatedLogicData[operatorName]) { - return true; - } - } - } - return false; -}; - -const convertQueriesToPrismaWhereClause = (logicData: LogicData) => { - const _isNegation = isNegation(logicData); - if (_isNegation) { - logicData = logicData["!"]; - } - for (const [key] of Object.entries(OPERATOR_MAP)) { - const operatorName = key as keyof typeof OPERATOR_MAP; - if (logicData[operatorName]) { - return convertSingleQueryToPrismaWhereClause(operatorName, logicData, _isNegation); - } - } -}; - -export const jsonLogicToPrisma = (query: JsonLogicQuery) => { - try { - let logic = query.logic; - if (!logic) { - return {}; - } - - let prismaWhere: PrismaWhere = {}; - let negateLogic = false; - - // Case: Negation of "Any of these" - // Example: {"logic":{"!":{"or":[{"==":[{"var":"505d3c3c-aa71-4220-93a9-6fd1e1087939"},"1"]},{"==":[{"var":"505d3c3c-aa71-4220-93a9-6fd1e1087939"},"1"]}]}}} - if (logic["!"]) { - logic = logic["!"]; - negateLogic = true; - } - - // Case: All of these - if (logic.and) { - const where: PrismaWhere["AND"] = (prismaWhere[GROUP_OPERATOR_MAP["and"]] = []); - logic.and.forEach((and) => { - const res = convertQueriesToPrismaWhereClause(and); - if (!res) { - return; - } - where.push(res); - }); - } - // Case: Any of these - else if (logic.or) { - const where: PrismaWhere["OR"] = (prismaWhere[GROUP_OPERATOR_MAP["or"]] = []); - - logic.or.forEach((or) => { - const res = convertQueriesToPrismaWhereClause(or); - if (!res) { - return; - } - where.push(res); - }); - } - - if (negateLogic) { - prismaWhere = { NOT: { ...prismaWhere } }; - } - - return prismaWhere; - } catch (e) { - console.log("Error converting to prisma `where`", JSON.stringify(query), "Error is ", e); - return {}; - } -}; diff --git a/packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts b/packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts index 84b5ac76a6..000ece889a 100644 --- a/packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts +++ b/packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts @@ -84,7 +84,7 @@ export function getQueryBuilderConfigForFormFields(form: Pick { - const handler = await getHandler("report", () => import("./report.handler")); - return handler({ ctx, input }); - }), - getAttributesForTeam: authedProcedure .input(ZGetAttributesForTeamInputSchema) .query(async ({ ctx, input }) => { diff --git a/packages/app-store/routing-forms/trpc/buildResponsesForReporting.test.ts b/packages/app-store/routing-forms/trpc/buildResponsesForReporting.test.ts deleted file mode 100644 index fb107cb868..0000000000 --- a/packages/app-store/routing-forms/trpc/buildResponsesForReporting.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; -import { describe, it, expect } from "vitest"; - -import { buildResponsesForReporting } from "./report.handler"; - -describe("buildResponsesForReporting", () => { - it("for fields with options, it should return the labels of the options", () => { - const field1Id = uuidv4(); - const field2Id = uuidv4(); - - const responsesFromDb = [ - { - [field1Id]: { value: "value1" }, - [field2Id]: { value: ["option1", "option2"] }, - }, - ]; - - const fields = [ - { id: field1Id, label: "Field 1" }, - { - id: field2Id, - label: "Field 2", - options: [ - { id: "option1", label: "Option 1" }, - { id: "option2", label: "Option 2" }, - ], - }, - ]; - - const expectedResponses = [["value1", "Option 1, Option 2"]]; - - const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); - expect(responses).toEqual(expectedResponses); - expect(headers).toEqual(["Field 1", "Field 2"]); - }); - - it("for fields with options having null id, it should return what DB provided(as that would be labels only).", () => { - const field1Id = uuidv4(); - const field2Id = uuidv4(); - const responsesFromDb = [ - { - [field1Id]: { value: "value1" }, - [field2Id]: { value: ["Option 1", "Option 2"] }, - }, - ]; - - const fields = [ - { id: field1Id, label: "Field 1" }, - { - id: field2Id, - label: "Field 2", - options: [ - { id: null, label: "Option 1" }, - { id: null, label: "Option 2" }, - ], - }, - ]; - - const expectedResponses = [["value1", "Option 1, Option 2"]]; - - const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); - expect(responses).toEqual(expectedResponses); - expect(headers).toEqual(["Field 1", "Field 2"]); - }); - - it("for fields with no options but having array value, it should return the value as is", () => { - const field2Id = uuidv4(); - const responsesFromDb = [ - { - [field2Id]: { value: ["Option 1", "Option 2"] }, - }, - ]; - - const fields = [ - { - id: field2Id, - label: "Field 2", - }, - ]; - - const expectedResponses = [["Option 1, Option 2"]]; - - const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); - expect(responses).toEqual(expectedResponses); - expect(headers).toEqual(["Field 2"]); - }); - - it("should correctly handle numeric responses converting them to strings", () => { - const field1Id = uuidv4(); - const field2Id = uuidv4(); - const responsesFromDb = [ - { - [field1Id]: { value: 1 }, - [field2Id]: { value: [1, 2] }, - }, - ]; - - const fields = [ - { id: field1Id, label: "Field 1" }, - { - id: field2Id, - label: "Field 2", - options: [ - { id: null, label: "Option 1" }, - { id: null, label: "Option 2" }, - ], - }, - ]; - const expectedResponses = [["1", "1, 2"]]; - const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); - expect(responses).toEqual(expectedResponses); - expect(headers).toEqual(["Field 1", "Field 2"]); - }); - - it("should handle empty responses", () => { - const field1Id = uuidv4(); - const responsesFromDb = [{}]; - - const fields = [{ id: field1Id, label: "Field 1" }]; - - const expectedResponses = [[""]]; - - const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); - expect(responses).toEqual(expectedResponses); - expect(headers).toEqual(["Field 1"]); - }); - - it("should show correct header for deleted fields", () => { - const field1Id = uuidv4(); - const field2Id = uuidv4(); - const responsesFromDb = [ - { - [field1Id]: { value: "value1" }, - [field2Id]: { value: ["Option 1", "Option 2"] }, - }, - ]; - - const fields = [ - { id: field1Id, label: "Field 1" }, - { - id: field2Id, - label: "Field 2", - deleted: true, - options: [ - { id: "option1", label: "Option 1" }, - { id: "option2", label: "Option 2" }, - ], - }, - ]; - - const expectedResponses = [["value1", "Option 1, Option 2"]]; - const expectedHeaders = ["Field 1", "Field 2(Deleted)"]; - - const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields }); - expect(responses).toEqual(expectedResponses); - expect(headers).toEqual(expectedHeaders); - }); -}); diff --git a/packages/app-store/routing-forms/trpc/report.handler.ts b/packages/app-store/routing-forms/trpc/report.handler.ts deleted file mode 100644 index d42d25bf9f..0000000000 --- a/packages/app-store/routing-forms/trpc/report.handler.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { z } from "zod"; - -import logger from "@calcom/lib/logger"; -import type { PrismaClient } from "@calcom/prisma"; -import type { App_RoutingForms_FormResponse } from "@calcom/prisma/client"; -import type { TrpcSessionUser } from "@calcom/trpc/server/types"; - -import { TRPCError } from "@trpc/server"; - -import { jsonLogicToPrisma } from "../jsonLogicToPrisma"; -import { getSerializableForm } from "../lib/getSerializableForm"; -import { ensureStringOrStringArray, getLabelsFromOptionIds } from "../lib/reportingUtils"; -import type { FormResponse } from "../types/types"; -import type { zodFieldView } from "../zod"; -import type { TReportInputSchema } from "./report.schema"; - -interface ReportHandlerOptions { - ctx: { - prisma: PrismaClient; - user: NonNullable; - }; - input: TReportInputSchema; -} - -const makeFormatDate = (locale: string, timeZone: string) => { - const formatDate = (date: Date): string => { - return new Intl.DateTimeFormat(locale, { - timeZone, - dateStyle: "medium", - timeStyle: "short", - }).format(date); - }; - return formatDate; -}; - -const getRows = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => { - // Can be any prisma `where` clause - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const prismaWhere: Record = input.jsonLogicQuery - ? jsonLogicToPrisma(input.jsonLogicQuery) - : {}; - const skip = input.cursor ?? 0; - const take = input.limit ? input.limit + 1 : 50; - - logger.debug( - `Built Prisma where ${JSON.stringify(prismaWhere)} from jsonLogicQuery ${JSON.stringify( - input.jsonLogicQuery - )}` - ); - const rows = await prisma.app_RoutingForms_FormResponse.findMany({ - where: { - formId: input.formId, - ...prismaWhere, - }, - include: { - routedToBooking: { - select: { - createdAt: true, - user: { - select: { id: true, name: true, email: true }, - }, - assignmentReason: { - orderBy: { - createdAt: "desc", - }, - take: 1, - }, - }, - }, - }, - orderBy: { - createdAt: "desc", - }, - take, - skip, - }); - return { skip, take, rows }; -}; - -export const reportHandler = async (options: ReportHandlerOptions) => { - const { - ctx: { prisma }, - input, - } = options; - const form = await prisma.app_RoutingForms_Form.findUnique({ - where: { - id: input.formId, - }, - }); - - if (!form) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Form not found", - }); - } - // TODO: Second argument is required to return deleted operators. - const serializedForm = await getSerializableForm({ form, withDeletedFields: true }); - const { rows, skip, take } = await getRows(options); - - const fields = serializedForm?.fields || []; - const { responses, headers } = buildResponsesForReporting({ - responsesFromDb: rows.map((r) => r.response), - fields, - }); - - const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take; - return presenter({ - rows, - options, - headers, - responses, - nextCursor: areThereNoResultsOrLessThanAskedFor ? null : skip + rows.length, - }); -}; - -/** - * This is a temporary solution to make the report work. It should be incorporated into the report data itself. - * Right now we cannot filter by Routed To and Booked At because they are not part of the response data. - */ -function presenter(args: { - rows: Awaited>["rows"]; - options: ReportHandlerOptions; - headers: string[]; - responses: string[][]; - nextCursor: number | null; -}) { - const { headers, responses, nextCursor, options, rows } = args; - const { ctx } = options; - const formatDate = makeFormatDate(ctx.user.locale, ctx.user.timeZone); - return { - nextCursor, - headers: [...headers, "Routed To", "Assignment Reason", "Booked At", "Submitted At"], - responses: responses.map((r, i) => { - const currentRow = rows[i]; - return [ - ...r, - currentRow.routedToBooking?.user?.email || "", - currentRow.routedToBooking?.assignmentReason.length - ? currentRow.routedToBooking.assignmentReason[0].reasonString - : "", - currentRow.routedToBooking?.createdAt ? formatDate(currentRow.routedToBooking.createdAt) : "", - formatDate(currentRow.createdAt), - ]; - }), - }; -} - -export default reportHandler; - -export function buildResponsesForReporting({ - responsesFromDb, - fields, -}: { - responsesFromDb: App_RoutingForms_FormResponse["response"][]; - fields: Pick, "id" | "options" | "label" | "deleted">[]; -}) { - const headers = fields.map((f) => f.label + (f.deleted ? "(Deleted)" : "")); - const responses: string[][] = []; - responsesFromDb.forEach((r) => { - const rowResponses: string[] = []; - responses.push(rowResponses); - fields.forEach((field) => { - if (!r) { - return; - } - const response = r as FormResponse; - const value = response[field.id]?.value || ""; - if (field.options) { - const optionIds = ensureStringOrStringArray(value); - const labels = getLabelsFromOptionIds({ options: field.options, optionIds }); - rowResponses.push(labels.join(", ")); - } else { - const arrayOfValues = value instanceof Array ? value : [value]; - const transformedValue = arrayOfValues.join(", "); - rowResponses.push(transformedValue); - } - }); - }); - - return { responses, headers }; -} diff --git a/packages/app-store/routing-forms/trpc/report.schema.ts b/packages/app-store/routing-forms/trpc/report.schema.ts deleted file mode 100644 index a7f0270d66..0000000000 --- a/packages/app-store/routing-forms/trpc/report.schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import z from "zod"; - -export const ZReportInputSchema = z.object({ - limit: z.number().default(50), - formId: z.string(), - jsonLogicQuery: z.object({ - logic: z.union([z.record(z.any()), z.null()]), - }), - cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type -}); - -export type TReportInputSchema = z.infer;