chore: Routing Forms - remove unused report.handler.ts and jsonLogicToPrisma functionality (#23032)
* feat: remove unused report.handler.ts and jsonLogicToPrisma functionality - Remove unused packages/app-store/routing-forms/trpc/report.handler.ts - Remove packages/app-store/routing-forms/jsonLogicToPrisma.ts implementation - Remove packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts test file These files were no longer being used in the routing-forms package. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: clean up remaining references to removed report functionality - Remove unused report.schema.ts file - Remove buildResponsesForReporting test file - Remove report endpoint from router - Update comment referencing jsonLogicToPrisma All references to the removed jsonLogicToPrisma and report handler functionality have been cleaned up. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Update packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
0a11c45065
commit
dfc5d914fc
7 files changed
+1
-998
No files matched your search
@@ -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" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<Record<keyof typeof OPERATOR_MAP, any>>;
|
||||
type NegatedLogicData = {
|
||||
"!": LogicData;
|
||||
};
|
||||
|
||||
export type JsonLogicQuery = {
|
||||
logic: {
|
||||
and?: LogicData[];
|
||||
or?: LogicData[];
|
||||
"!"?: {
|
||||
and?: LogicData[];
|
||||
or?: LogicData[];
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PrismaWhere = {
|
||||
AND?: ReturnType<typeof convertQueriesToPrismaWhereClause>[];
|
||||
OR?: ReturnType<typeof convertQueriesToPrismaWhereClause>[];
|
||||
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 {};
|
||||
}
|
||||
};
|
||||
@@ -84,7 +84,7 @@ export function getQueryBuilderConfigForFormFields(form: Pick<RoutingForm, "fiel
|
||||
delete initialConfigCopy.operators.is_empty;
|
||||
delete initialConfigCopy.operators.is_not_empty;
|
||||
|
||||
// Between and Not between aren't directly supported by prisma. So, we need to update jsonLogicToPrisma to generate gte and lte query for between. It can be implemented later.
|
||||
// These operators can be implemented later if needed.
|
||||
delete initialConfigCopy.operators.between;
|
||||
delete initialConfigCopy.operators.not_between;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ZFormQueryInputSchema } from "./formQuery.schema";
|
||||
import { ZGetAttributesForTeamInputSchema } from "./getAttributesForTeam.schema";
|
||||
import { ZGetIncompleteBookingSettingsInputSchema } from "./getIncompleteBookingSettings.schema";
|
||||
import { forms } from "./procedures/forms";
|
||||
import { ZReportInputSchema } from "./report.schema";
|
||||
import { ZSaveIncompleteBookingSettingsInputSchema } from "./saveIncompleteBookingSettings.schema";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@@ -76,11 +75,6 @@ const appRoutingForms = router({
|
||||
return handler({ ctx, input });
|
||||
}),
|
||||
|
||||
report: authedProcedure.input(ZReportInputSchema).query(async ({ ctx, input }) => {
|
||||
const handler = await getHandler("report", () => import("./report.handler"));
|
||||
return handler({ ctx, input });
|
||||
}),
|
||||
|
||||
getAttributesForTeam: authedProcedure
|
||||
.input(ZGetAttributesForTeamInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<TrpcSessionUser>;
|
||||
};
|
||||
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<string, any> = 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<ReturnType<typeof getRows>>["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<z.infer<typeof zodFieldView>, "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 };
|
||||
}
|
||||
@@ -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<typeof ZReportInputSchema>;
|
||||
Reference in new issue
Block a user