Files
calendar/packages/features/data-table/lib/server.ts
T
Eunjae LeeGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
10fca86ddc feat: add drop off funnel chart for /insights/routing (#22106)
* feat: add Drop-off funnel chart for /insights/routing

* convert insights services to use kysely

* WIP

* implement drop-off data WIP

* updates

* revert services back to Prisma

* do not expose findMany

* WIP

* revert usage

* fix type errors

* updates

* clean up

* update playground

* update styles

* support columnFilters

* update tooltip

* add skeleton

* remove dynamic loading

* avoid using Prisma.raw

* update condition for user scope

* Update packages/features/insights/components/RoutingFunnelContent.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* playground fix

* fix type error

* update playground

* update styles

* implement subtitle to ChartCard

* revert insights booking service

* update border

* update ChartCard style

* style updates

* convert the playground to a client component

* clean up

* apply feedback

* fix tests

* fix unit test

* hide scrollbar

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-07-16 16:36:41 +02:00

232 lines
6.1 KiB
TypeScript

import { Prisma } from "@prisma/client";
import type { FilterValue, SortingState } from "./types";
import {
isSingleSelectFilterValue,
isMultiSelectFilterValue,
isTextFilterValue,
isNumberFilterValue,
isDateRangeFilterValue,
} from "./utils";
type MakeWhereClauseProps = {
columnName: string;
filterValue: FilterValue;
json?: true | { path: string[] };
};
export function makeOrderBy(sorting: SortingState) {
if (!sorting || !sorting.length) return undefined;
return sorting.map((sort) => ({
[sort.id]: sort.desc ? ("desc" as const) : ("asc" as const),
}));
}
/**
* Builds a Prisma where clause for use with Prisma queries
*/
export function makeWhereClause(props: MakeWhereClauseProps) {
const { columnName, filterValue } = props;
const isJson = props.json === true || (typeof props.json === "object" && props.json.path?.length > 0);
const jsonPath = isJson && typeof props.json === "object" ? props.json.path : undefined;
const jsonPathObj = isJson && jsonPath ? { path: jsonPath } : {};
if (isMultiSelectFilterValue(filterValue)) {
return {
[columnName]: {
...jsonPathObj,
...(isJson ? { array_contains: filterValue.data } : { in: filterValue.data }),
},
};
} else if (isSingleSelectFilterValue(filterValue)) {
return {
[columnName]: {
...jsonPathObj,
equals: filterValue.data,
},
};
} else if (isTextFilterValue(filterValue)) {
const { operator, operand } = filterValue.data;
switch (operator) {
case "equals":
return {
[columnName]: {
...jsonPathObj,
equals: operand,
},
};
case "notEquals":
return {
[columnName]: {
...jsonPathObj,
not: operand,
},
};
case "contains":
return {
[columnName]: {
...jsonPathObj,
...(isJson ? { string_contains: operand } : { contains: operand, mode: "insensitive" }),
},
};
case "notContains":
return {
NOT: {
[columnName]: {
...jsonPathObj,
...(isJson ? { string_contains: operand } : { contains: operand, mode: "insensitive" }),
},
},
};
case "startsWith":
return {
[columnName]: {
...jsonPathObj,
...(isJson ? { string_starts_with: operand } : { startsWith: operand, mode: "insensitive" }),
},
};
case "endsWith":
return {
[columnName]: {
...jsonPathObj,
...(isJson ? { string_ends_with: operand } : { endsWith: operand, mode: "insensitive" }),
},
};
case "isEmpty":
return {
[columnName]: {
...jsonPathObj,
equals: "",
},
};
case "isNotEmpty":
return {
NOT: {
[columnName]: {
...jsonPathObj,
equals: "",
},
},
};
default:
throw new Error(`Invalid operator for text filter: ${operator}`);
}
} else if (isNumberFilterValue(filterValue)) {
const { operator, operand } = filterValue.data;
switch (operator) {
case "eq":
return {
[columnName]: {
...jsonPathObj,
equals: operand,
},
};
case "neq":
return {
[columnName]: {
...jsonPathObj,
not: operand,
},
};
case "gt":
return {
[columnName]: {
...jsonPathObj,
gt: operand,
},
};
case "gte":
return {
[columnName]: {
...jsonPathObj,
gte: operand,
},
};
case "lt":
return {
[columnName]: {
...jsonPathObj,
lt: operand,
},
};
case "lte":
return {
[columnName]: {
...jsonPathObj,
lte: operand,
},
};
default:
throw new Error(`Invalid operator for number filter: ${operator}`);
}
} else if (isDateRangeFilterValue(filterValue)) {
const { startDate, endDate } = filterValue.data;
if (!startDate || !endDate) {
throw new Error(`Invalid date range filter: ${JSON.stringify({ columnName, startDate, endDate })}`);
}
return {
[columnName]: {
...jsonPathObj,
gte: startDate,
lte: endDate,
},
};
}
throw new Error(`Invalid filter type: ${JSON.stringify({ columnName, filterValue })}`);
}
/**
* Builds a SQL where clause for use with raw SQL queries
*/
export function makeSqlCondition(filterValue: FilterValue): Prisma.Sql | null {
if (isMultiSelectFilterValue(filterValue)) {
return Prisma.sql`= ANY(${filterValue.data})`;
} else if (isSingleSelectFilterValue(filterValue)) {
return Prisma.sql`= ${filterValue.data}`;
} else if (isTextFilterValue(filterValue)) {
const { operator, operand } = filterValue.data;
switch (operator) {
case "equals":
return Prisma.sql`= ${operand}`;
case "notEquals":
return Prisma.sql`!= ${operand}`;
case "contains":
return Prisma.sql`ILIKE ${`%${operand}%`}`;
case "notContains":
return Prisma.sql`NOT ILIKE ${`%${operand}%`}`;
case "startsWith":
return Prisma.sql`ILIKE ${`${operand}%`}`;
case "endsWith":
return Prisma.sql`ILIKE ${`%${operand}`}`;
case "isEmpty":
return Prisma.sql`= ''`;
case "isNotEmpty":
return Prisma.sql`!= ''`;
default:
return null;
}
} else if (isNumberFilterValue(filterValue)) {
const { operator, operand } = filterValue.data;
switch (operator) {
case "eq":
return Prisma.sql`= ${operand}`;
case "neq":
return Prisma.sql`!= ${operand}`;
case "gt":
return Prisma.sql`> ${operand}`;
case "gte":
return Prisma.sql`>= ${operand}`;
case "lt":
return Prisma.sql`< ${operand}`;
case "lte":
return Prisma.sql`<= ${operand}`;
default:
return null;
}
}
return null;
}