10fca86ddc
* 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>
232 lines
6.1 KiB
TypeScript
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;
|
|
}
|