Refactor filter handling for queries.

This commit is contained in:
Mike Cao 2025-07-02 01:44:12 -07:00
parent 5b300f1ff5
commit ee6c68d27c
107 changed files with 731 additions and 835 deletions

View file

@ -15,7 +15,7 @@ jobs:
strategy: strategy:
matrix: matrix:
db-type: [postgresql, mysql] db-type: [postgresql]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -55,4 +55,4 @@ jobs:
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -10,7 +10,7 @@ jobs:
strategy: strategy:
matrix: matrix:
db-type: [postgresql, mysql] db-type: [postgresql]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -47,4 +47,4 @@ jobs:
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -2,6 +2,7 @@
import { Button, Grid, Column, Heading } from '@umami/react-zen'; import { Button, Grid, Column, Heading } from '@umami/react-zen';
import Link from 'next/link'; import Link from 'next/link';
import Script from 'next/script'; import Script from 'next/script';
import { Panel } from '@/components/common/Panel';
import { PageBody } from '@/components/common/PageBody'; import { PageBody } from '@/components/common/PageBody';
import { EventsChart } from '@/components/metrics/EventsChart'; import { EventsChart } from '@/components/metrics/EventsChart';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
@ -115,87 +116,91 @@ export function TestConsolePage({ websiteId }: { websiteId: string }) {
src={`${process.env.basePath || ''}/script.js`} src={`${process.env.basePath || ''}/script.js`}
data-cache="true" data-cache="true"
/> />
<Grid columns="1fr 1fr 1fr" gap> <Panel>
<Column gap> <Grid columns="1fr 1fr 1fr" gap>
<Heading>Page links</Heading> <Column gap>
<div> <Heading>Page links</Heading>
<Link href={`/console/${websiteId}?page=1`}>page one</Link> <div>
</div> <Link href={`/console/${websiteId}?page=1`}>page one</Link>
<div>
<Link href={`/console/${websiteId}?page=2 `}>page two</Link>
</div>
<div>
<a href="https://www.google.com" data-umami-event="external-link-direct">
external link (direct)
</a>
</div>
<div>
<a
href="https://www.google.com"
data-umami-event="external-link-tab"
target="_blank"
rel="noreferrer"
>
external link (tab)
</a>
</div>
</Column>
<Column gap>
<Heading>Click events</Heading>
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
Send event
</Button>
<Button
id="send-event-data-button"
data-umami-event="button-click"
data-umami-event-name="bob"
data-umami-event-id="123"
variant="primary"
>
Send event with data
</Button>
<Button
id="generate-revenue-button"
data-umami-event="checkout-cart"
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
data-umami-event-currency="USD"
variant="primary"
>
Generate revenue data
</Button>
<Button
id="button-with-div-button"
data-umami-event="button-click"
data-umami-event-name={'bob'}
data-umami-event-id="123"
variant="primary"
>
<div>Button with div</div>
</Button>
<div data-umami-event="div-click">DIV with attribute</div>
<div data-umami-event="div-click-one">
<div data-umami-event="div-click-two">
<div data-umami-event="div-click-three">Nested DIV</div>
</div> </div>
</div> <div>
</Column> <Link href={`/console/${websiteId}?page=2 `}>page two</Link>
<Column gap> </div>
<Heading>Javascript events</Heading> <div>
<Button id="manual-button" variant="primary" onClick={handleRunScript}> <a href="https://www.google.com" data-umami-event="external-link-direct">
Run script external link (direct)
</Button> </a>
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}> </div>
Run identify <div>
</Button> <a
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}> href="https://www.google.com"
Revenue script data-umami-event="external-link-tab"
</Button> target="_blank"
</Column> rel="noreferrer"
</Grid> >
external link (tab)
</a>
</div>
</Column>
<Column gap>
<Heading>Click events</Heading>
<Button id="send-event-button" data-umami-event="button-click" variant="primary">
Send event
</Button>
<Button
id="send-event-data-button"
data-umami-event="button-click"
data-umami-event-name="bob"
data-umami-event-id="123"
variant="primary"
>
Send event with data
</Button>
<Button
id="generate-revenue-button"
data-umami-event="checkout-cart"
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
data-umami-event-currency="USD"
variant="primary"
>
Generate revenue data
</Button>
<Button
id="button-with-div-button"
data-umami-event="button-click"
data-umami-event-name={'bob'}
data-umami-event-id="123"
variant="primary"
>
<div>Button with div</div>
</Button>
<div data-umami-event="div-click">DIV with attribute</div>
<div data-umami-event="div-click-one">
<div data-umami-event="div-click-two">
<div data-umami-event="div-click-three">Nested DIV</div>
</div>
</div>
</Column>
<Column gap>
<Heading>Javascript events</Heading>
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
Run script
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
Run identify
</Button>
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
Revenue script
</Button>
</Column>
</Grid>
</Panel>
<Heading>Pageviews</Heading> <Heading>Pageviews</Heading>
<WebsiteChart websiteId={websiteId} /> <WebsiteChart websiteId={websiteId} />
<Heading>Events</Heading> <Heading>Events</Heading>
<EventsChart websiteId={websiteId} /> <Panel>
<EventsChart websiteId={websiteId} />
</Panel>
</Column> </Column>
</PageBody> </PageBody>
); );

View file

@ -24,7 +24,7 @@ export function PasswordEditForm({ onSave, onClose }) {
}); });
}; };
const samePassword = (value: string, values: { [key: string]: any }) => { const samePassword = (value: string, values: Record<string, any>) => {
if (value !== values.newPassword) { if (value !== values.newPassword) {
return formatMessage(messages.noMatchPassword); return formatMessage(messages.noMatchPassword);
} }

View file

@ -1,9 +1,9 @@
import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable'; import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid'; import { DataGrid } from '@/components/common/DataGrid';
import { useWebsites } from '@/components/hooks'; import { useWebsitesQuery } from '@/components/hooks';
export function UserWebsites({ userId }) { export function UserWebsites({ userId }) {
const queryResult = useWebsites({ userId }); const queryResult = useWebsitesQuery({ userId });
return ( return (
<DataGrid queryResult={queryResult}> <DataGrid queryResult={queryResult}>

View file

@ -1,7 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable'; import { WebsitesTable } from '@/app/(main)/settings/websites/WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid'; import { DataGrid } from '@/components/common/DataGrid';
import { useWebsites } from '@/components/hooks'; import { useWebsitesQuery } from '@/components/hooks';
export function WebsitesDataTable({ export function WebsitesDataTable({
teamId, teamId,
@ -16,10 +16,10 @@ export function WebsitesDataTable({
showActions?: boolean; showActions?: boolean;
children?: ReactNode; children?: ReactNode;
}) { }) {
const queryResult = useWebsites({ teamId }); const queryResult = useWebsitesQuery({ teamId });
return ( return (
<DataGrid queryResult={queryResult} renderEmpty={() => children}> <DataGrid queryResult={queryResult} renderEmpty={() => children} allowSearch allowPaging>
{({ data }) => ( {({ data }) => (
<WebsitesTable <WebsitesTable
teamId={teamId} teamId={teamId}

View file

@ -31,7 +31,7 @@ export function WebsitesTable({
return ( return (
<DataTable data={data}> <DataTable data={data}>
<DataColumn id="name" label={formatMessage(labels.name)}> <DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => <Link href={renderUrl(`/websites/${row.id}`)}>{row.name}</Link>} {(row: any) => <Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>}
</DataColumn> </DataColumn>
<DataColumn id="domain" label={formatMessage(labels.domain)} /> <DataColumn id="domain" label={formatMessage(labels.domain)} />
{showActions && ( {showActions && (

View file

@ -3,7 +3,6 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PageviewsChart } from '@/components/metrics/PageviewsChart'; import { PageviewsChart } from '@/components/metrics/PageviewsChart';
import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery'; import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery';
import { useDateRange } from '@/components/hooks'; import { useDateRange } from '@/components/hooks';
import { Panel } from '@/components/common/Panel';
export function WebsiteChart({ export function WebsiteChart({
websiteId, websiteId,
@ -48,16 +47,14 @@ export function WebsiteChart({
}, [data, startDate, endDate, unit]); }, [data, startDate, endDate, unit]);
return ( return (
<Panel height="520px"> <LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}>
<LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}> <PageviewsChart
<PageviewsChart key={value}
key={value} data={chartData}
data={chartData} minDate={startDate}
minDate={startDate} maxDate={endDate}
maxDate={endDate} unit={unit}
unit={unit} />
/> </LoadingPanel>
</LoadingPanel>
</Panel>
); );
} }

View file

@ -1,6 +1,7 @@
'use client'; 'use client';
import { Column } from '@umami/react-zen'; import { Column } from '@umami/react-zen';
import { useNavigation } from '@/components/hooks'; import { useNavigation } from '@/components/hooks';
import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from './WebsiteChart'; import { WebsiteChart } from './WebsiteChart';
import { WebsiteExpandedView } from './WebsiteExpandedView'; import { WebsiteExpandedView } from './WebsiteExpandedView';
import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { WebsiteMetricsBar } from './WebsiteMetricsBar';
@ -16,8 +17,10 @@ export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
return ( return (
<Column gap> <Column gap>
<WebsiteControls websiteId={websiteId} allowCompare={true} /> <WebsiteControls websiteId={websiteId} allowCompare={true} />
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} /> <WebsiteMetricsBar websiteId={websiteId} showChange={true} />
<WebsiteChart websiteId={websiteId} compareMode={compare} /> <Panel>
<WebsiteChart websiteId={websiteId} compareMode={compare} />
</Panel>
{!view && !compare && <WebsiteTableView websiteId={websiteId} />} {!view && !compare && <WebsiteTableView websiteId={websiteId} />}
{view && !compare && <WebsiteExpandedView websiteId={websiteId} />} {view && !compare && <WebsiteExpandedView websiteId={websiteId} />}
{compare && <WebsiteCompareTables websiteId={websiteId} />} {compare && <WebsiteCompareTables websiteId={websiteId} />}

View file

@ -11,13 +11,12 @@ import {
import { Fragment } from 'react'; import { Fragment } from 'react';
import { More, Share, Edit } from '@/components/icons'; import { More, Share, Edit } from '@/components/icons';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { InputItem } from '@/lib/types';
export function WebsiteMenu({ websiteId }: { websiteId: string }) { export function WebsiteMenu({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { router, updateParams, renderUrl } = useNavigation(); const { router, updateParams, renderUrl } = useNavigation();
const menuItems: InputItem[] = [ const menuItems = [
{ id: 'share', label: formatMessage(labels.share), icon: <Share /> }, { id: 'share', label: formatMessage(labels.share), icon: <Share /> },
{ id: 'edit', label: formatMessage(labels.edit), icon: <Edit />, seperator: true }, { id: 'edit', label: formatMessage(labels.edit), icon: <Edit />, seperator: true },
]; ];

View file

@ -3,26 +3,18 @@ import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar'; import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatShortTime, formatLongNumber } from '@/lib/format'; import { formatShortTime, formatLongNumber } from '@/lib/format';
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery'; import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
import { useWebsites } from '@/store/websites';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
export function WebsiteMetricsBar({ export function WebsiteMetricsBar({
websiteId, websiteId,
showChange = false,
compareMode = false,
}: { }: {
websiteId: string; websiteId: string;
showChange?: boolean; showChange?: boolean;
compareMode?: boolean; compareMode?: boolean;
showFilter?: boolean;
}) { }) {
const { dateRange } = useDateRange(websiteId); const { dateRange } = useDateRange(websiteId);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(
websiteId,
compareMode && dateCompare,
);
const isAllTime = dateRange.value === 'all'; const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {}; const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {};
@ -87,8 +79,7 @@ export function WebsiteMetricsBar({
change={change} change={change}
formatValue={formatValue} formatValue={formatValue}
reverseColors={reverseColors} reverseColors={reverseColors}
showChange={!isAllTime && (compareMode || showChange)} showChange={!isAllTime}
showPrevious={!isAllTime && compareMode}
/> />
); );
})} })}

View file

@ -9,7 +9,7 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
export function GoalsPage({ websiteId }: { websiteId: string }) { export function GoalsPage({ websiteId }: { websiteId: string }) {
const { result, query } = useReportsQuery({ websiteId, type: 'goal' }); const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange(websiteId);
@ -20,14 +20,16 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
<SectionHeader> <SectionHeader>
<GoalAddButton websiteId={websiteId} /> <GoalAddButton websiteId={websiteId} />
</SectionHeader> </SectionHeader>
<LoadingPanel data={result?.data} isLoading={query?.isLoading} error={query?.error}> <LoadingPanel data={data} isLoading={isLoading} error={error}>
<Grid columns="1fr 1fr" gap> {data && (
{result?.data?.map((report: any) => ( <Grid columns="1fr 1fr" gap>
<Panel key={report.id}> {data['data'].map((report: any) => (
<Goal {...report} startDate={startDate} endDate={endDate} /> <Panel key={report.id}>
</Panel> <Goal {...report} startDate={startDate} endDate={endDate} />
))} </Panel>
</Grid> ))}
</Grid>
)}
</LoadingPanel> </LoadingPanel>
</Column> </Column>
); );

View file

@ -73,7 +73,7 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
); );
} }
function toArray(data: { [key: string]: number } = {}) { function toArray(data: Record<string, number> = {}) {
return Object.keys(data) return Object.keys(data)
.map(key => { .map(key => {
return { name: key, value: data[key] }; return { name: key, value: data[key] };

View file

@ -14,7 +14,7 @@ export function SessionsDataTable({
const queryResult = useWebsiteSessionsQuery(websiteId); const queryResult = useWebsiteSessionsQuery(websiteId);
return ( return (
<DataGrid queryResult={queryResult} allowSearch={true} renderEmpty={() => children}> <DataGrid queryResult={queryResult} renderEmpty={() => children} allowPaging>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />} {({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
</DataGrid> </DataGrid>
); );

View file

@ -1,12 +1,12 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen'; import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen';
import { useFormat, useLocale, useMessages, useRegionNames, useTimezone } from '@/components/hooks'; import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
import { TypeIcon } from '@/components/common/TypeIcon'; import { TypeIcon } from '@/components/common/TypeIcon';
import { Location, KeyRound, Calendar } from '@/components/icons'; import { Location, KeyRound, Calendar } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
export function SessionInfo({ data }) { export function SessionInfo({ data }) {
const { locale } = useLocale(); const { locale } = useLocale();
const { formatTimezoneDate } = useTimezone();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { getRegionName } = useRegionNames(locale); const { getRegionName } = useRegionNames(locale);
@ -22,11 +22,11 @@ export function SessionInfo({ data }) {
</Info> </Info>
<Info label={formatMessage(labels.lastSeen)} icon={<Calendar />}> <Info label={formatMessage(labels.lastSeen)} icon={<Calendar />}>
{formatTimezoneDate(data?.lastAt, 'PPPPpp')} <DateDistance date={new Date(data.lastAt)} />
</Info> </Info>
<Info label={formatMessage(labels.firstSeen)} icon={<Calendar />}> <Info label={formatMessage(labels.firstSeen)} icon={<Calendar />}>
{formatTimezoneDate(data?.firstAt, 'PPPPpp')} <DateDistance date={new Date(data.firstAt)} />
</Info> </Info>
<Info <Info

View file

@ -3,7 +3,7 @@ import { getRealtimeData } from '@/queries';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { startOfMinute, subMinutes } from 'date-fns'; import { startOfMinute, subMinutes } from 'date-fns';
import { REALTIME_RANGE } from '@/lib/constants'; import { REALTIME_RANGE } from '@/lib/constants';
import { parseRequest } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
export async function GET( export async function GET(
request: Request, request: Request,
@ -16,15 +16,19 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { timezone } = query;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); const filters = await getQueryFilters({
...query,
websiteId,
startAt: subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(),
endAt: Date.now(),
});
const data = await getRealtimeData(websiteId, { startDate, timezone }); const data = await getRealtimeData(websiteId, filters);
return json(data); return json(data);
} }

View file

@ -1,6 +1,6 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { getGoal } from '@/queries/sql/reports/getGoal'; import { getGoal } from '@/queries/sql/reports/getGoal';
import { reportResultSchema } from '@/lib/schema'; import { reportResultSchema } from '@/lib/schema';
@ -15,21 +15,22 @@ export async function POST(request: Request) {
websiteId, websiteId,
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
parameters: { type, value, property, operator }, parameters: { type, value, property, operator },
...filters
} = body; } = body;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const filters = await getQueryFilters(body.filters);
const data = await getGoal(websiteId, { const data = await getGoal(websiteId, {
...filters,
startDate: new Date(startDate), startDate: new Date(startDate),
endDate: new Date(endDate), endDate: new Date(endDate),
type, type,
value, value,
property, property,
operator, operator,
filters,
}); });
return json(data); return json(data);

View file

@ -1,7 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewTeam } from '@/lib/auth'; import { canViewTeam } from '@/lib/auth';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { pagingParams } from '@/lib/schema'; import { pagingParams } from '@/lib/schema';
import { getTeamWebsites } from '@/queries'; import { getTeamWebsites } from '@/queries';
@ -20,7 +20,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
return unauthorized(); return unauthorized();
} }
const websites = await getTeamWebsites(teamId, query); const filters = await getQueryFilters(query);
const websites = await getTeamWebsites(teamId, filters);
return json(websites); return json(websites);
} }

View file

@ -3,7 +3,7 @@ import { json, unauthorized } from '@/lib/response';
import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website'; import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website';
import { getEventUsage } from '@/queries/sql/events/getEventUsage'; import { getEventUsage } from '@/queries/sql/events/getEventUsage';
import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage'; import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage';
import { parseRequest, getRequestDateRange } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({ const schema = z.object({
@ -22,14 +22,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
} }
const { userId } = await params; const { userId } = await params;
const { startDate, endDate } = await getRequestDateRange(query); const filters = await getQueryFilters(query);
const websites = await getAllUserWebsitesIncludingTeamOwner(userId); const websites = await getAllUserWebsitesIncludingTeamOwner(userId);
const websiteIds = websites.map(a => a.id); const websiteIds = websites.map(a => a.id);
const websiteEventUsage = await getEventUsage(websiteIds, startDate, endDate); const websiteEventUsage = await getEventUsage(websiteIds, filters);
const eventDataUsage = await getEventDataUsage(websiteIds, startDate, endDate); const eventDataUsage = await getEventDataUsage(websiteIds, filters);
const websiteUsage = websites.map(a => ({ const websiteUsage = websites.map(a => ({
websiteId: a.id, websiteId: a.id,

View file

@ -2,7 +2,7 @@ import { z } from 'zod';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { getUserWebsites } from '@/queries/prisma/website'; import { getUserWebsites } from '@/queries/prisma/website';
import { pagingParams } from '@/lib/schema'; import { pagingParams } from '@/lib/schema';
import { parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({ const schema = z.object({
@ -21,7 +21,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
return unauthorized(); return unauthorized();
} }
const websites = await getUserWebsites(userId, query); const filters = await getQueryFilters(query);
const websites = await getUserWebsites(userId, filters);
return json(websites); return json(websites);
} }

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents'; import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents';
@ -20,16 +20,16 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { event } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const { event } = query;
const filters = await getQueryFilters(query);
const data = await getEventDataEvents(websiteId, { const data = await getEventDataEvents(websiteId, {
startDate, ...filters,
endDate,
event, event,
}); });

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getEventDataFields } from '@/queries'; import { getEventDataFields } from '@/queries';
@ -20,16 +20,14 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getEventDataFields(websiteId, { const filters = await getQueryFilters(query);
startDate,
endDate, const data = await getEventDataFields(websiteId, filters);
});
return json(data); return json(data);
} }

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getEventDataProperties } from '@/queries'; import { getEventDataProperties } from '@/queries';
@ -21,14 +21,15 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { propertyName } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getEventDataProperties(websiteId, { startDate, endDate, propertyName }); const { propertyName } = query;
const filters = await getQueryFilters(query);
const data = await getEventDataProperties(websiteId, { ...filters, propertyName });
return json(data); return json(data);
} }

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getEventDataStats } from '@/queries'; import { getEventDataStats } from '@/queries';
@ -21,13 +21,14 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getEventDataStats(websiteId, { startDate, endDate }); const filters = await getQueryFilters(query);
const data = await getEventDataStats(websiteId, filters);
return json(data); return json(data);
} }

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getEventDataValues } from '@/queries'; import { getEventDataValues } from '@/queries';
@ -22,16 +22,16 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { eventName, propertyName } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const { eventName, propertyName } = query;
const filters = await getQueryFilters(query);
const data = await getEventDataValues(websiteId, { const data = await getEventDataValues(websiteId, {
startDate, ...filters,
endDate,
eventName, eventName,
propertyName, propertyName,
}); });

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema'; import { pagingParams } from '@/lib/schema';
@ -22,13 +22,14 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getWebsiteEvents(websiteId, { ...query, startDate, endDate }, query); const filters = await getQueryFilters(query);
const data = await getWebsiteEvents(websiteId, filters);
return json(data); return json(data);
} }

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; import { filterParams, timezoneParam, unitParam } from '@/lib/schema';
@ -24,20 +24,12 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { timezone } = query;
const { startDate, endDate, unit } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const filters = { const filters = await getQueryFilters({ ...query, websiteId });
...getRequestFilters(query),
startDate,
endDate,
timezone,
unit,
};
const data = await getEventMetrics(websiteId, filters); const data = await getEventMetrics(websiteId, filters);

View file

@ -13,7 +13,7 @@ import {
VIDEO_DOMAINS, VIDEO_DOMAINS,
PAID_AD_PARAMS, PAID_AD_PARAMS,
} from '@/lib/constants'; } from '@/lib/constants';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response'; import { json, unauthorized, badRequest } from '@/lib/response';
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries'; import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
import { filterParams } from '@/lib/schema'; import { filterParams } from '@/lib/schema';
@ -45,13 +45,8 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const { startDate, endDate } = await getRequestDateRange(query);
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const filters = { const filters = await getQueryFilters({ ...query, websiteId });
...getRequestFilters(query),
startDate,
endDate,
};
if (search) { if (search) {
filters[type] = { filters[type] = {

View file

@ -1,7 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unitParam, timezoneParam, filterParams } from '@/lib/schema'; import { dateRangeParams, filterParams } from '@/lib/schema';
import { getCompareDate } from '@/lib/date'; import { getCompareDate } from '@/lib/date';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { getPageviewStats, getSessionStats } from '@/queries'; import { getPageviewStats, getSessionStats } from '@/queries';
@ -11,11 +11,7 @@ export async function GET(
{ params }: { params: Promise<{ websiteId: string }> }, { params }: { params: Promise<{ websiteId: string }> },
) { ) {
const schema = z.object({ const schema = z.object({
startAt: z.coerce.number().int(), ...dateRangeParams,
endAt: z.coerce.number().int(),
unit: unitParam,
timezone: timezoneParam,
compare: z.string().optional(),
...filterParams, ...filterParams,
}); });
@ -26,32 +22,23 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { timezone, compare } = query;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const { startDate, endDate, unit } = await getRequestDateRange(query); const filters = await getQueryFilters({ ...query, websiteId });
const filters = {
...getRequestFilters(query),
startDate,
endDate,
timezone,
unit,
};
const [pageviews, sessions] = await Promise.all([ const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, filters), getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters), getSessionStats(websiteId, filters),
]); ]);
if (compare) { if (filters.compare) {
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare, filters.compare,
startDate, filters.startDate,
endDate, filters.endDate,
); );
const [comparePageviews, compareSessions] = await Promise.all([ const [comparePageviews, compareSessions] = await Promise.all([
@ -70,8 +57,8 @@ export async function GET(
return json({ return json({
pageviews, pageviews,
sessions, sessions,
startDate, startDate: filters.startDate,
endDate, endDate: filters.endDate,
compare: { compare: {
pageviews: comparePageviews, pageviews: comparePageviews,
sessions: compareSessions, sessions: compareSessions,

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getSessionDataProperties } from '@/queries'; import { getSessionDataProperties } from '@/queries';
@ -22,13 +22,13 @@ export async function GET(
const { websiteId } = await params; const { websiteId } = await params;
const { propertyName } = query; const { propertyName } = query;
const { startDate, endDate } = await getRequestDateRange(query); const filters = await getQueryFilters(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getSessionDataProperties(websiteId, { startDate, endDate, propertyName }); const data = await getSessionDataProperties(websiteId, { ...filters, propertyName });
return json(data); return json(data);
} }

View file

@ -1,5 +1,5 @@
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { getSessionDataValues } from '@/queries'; import { getSessionDataValues } from '@/queries';
import { z } from 'zod'; import { z } from 'zod';
@ -22,15 +22,14 @@ export async function GET(
const { propertyName } = query; const { propertyName } = query;
const { websiteId } = await params; const { websiteId } = await params;
const { startDate, endDate } = await getRequestDateRange(query); const filters = await getQueryFilters({ ...query, websiteId });
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getSessionDataValues(websiteId, { const data = await getSessionDataValues(websiteId, {
startDate, ...filters,
endDate,
propertyName, propertyName,
}); });

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getRequestDateRange } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getSessionActivity } from '@/queries'; import { getSessionActivity } from '@/queries';
@ -20,13 +20,14 @@ export async function GET(
} }
const { websiteId, sessionId } = await params; const { websiteId, sessionId } = await params;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getSessionActivity(websiteId, sessionId, startDate, endDate); const filters = await getQueryFilters(query);
const data = await getSessionActivity(websiteId, sessionId, filters);
return json(data); return json(data);
} }

View file

@ -1,8 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { pagingParams } from '@/lib/schema'; import { dateRangeParams, filterParams, pagingParams } from '@/lib/schema';
import { getWebsiteSessions } from '@/queries'; import { getWebsiteSessions } from '@/queries';
export async function GET( export async function GET(
@ -10,8 +10,8 @@ export async function GET(
{ params }: { params: Promise<{ websiteId: string }> }, { params }: { params: Promise<{ websiteId: string }> },
) { ) {
const schema = z.object({ const schema = z.object({
startAt: z.coerce.number().int(), ...dateRangeParams,
endAt: z.coerce.number().int(), ...filterParams,
...pagingParams, ...pagingParams,
}); });
@ -21,14 +21,15 @@ export async function GET(
return error(); return error();
} }
const { startDate, endDate } = await getRequestDateRange(query);
const { websiteId } = await params; const { websiteId } = await params;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getWebsiteSessions(websiteId, { startDate, endDate }, query); const filters = await getQueryFilters({ ...query, websiteId });
const data = await getWebsiteSessions(websiteId, filters);
return json(data); return json(data);
} }

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { filterParams } from '@/lib/schema'; import { filterParams } from '@/lib/schema';
@ -27,15 +27,9 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const { startDate, endDate } = await getRequestDateRange(query); const filters = await getQueryFilters(query);
const filters = getRequestFilters(query); const metrics = await getWebsiteSessionStats(websiteId, filters);
const metrics = await getWebsiteSessionStats(websiteId, {
...filters,
startDate,
endDate,
});
const data = Object.keys(metrics[0]).reduce((obj, key) => { const data = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = { obj[key] = {

View file

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { getRequestDateRange, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { pagingParams, timezoneParam } from '@/lib/schema'; import { pagingParams, timezoneParam } from '@/lib/schema';
@ -23,14 +23,14 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { timezone } = query;
const { startDate, endDate } = await getRequestDateRange(query);
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const data = await getWebsiteSessionsWeekly(websiteId, { startDate, endDate, timezone }); const filters = await getQueryFilters(query);
const data = await getWebsiteSessionsWeekly(websiteId, filters);
return json(data); return json(data);
} }

View file

@ -1,8 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { parseRequest, getRequestDateRange, getRequestFilters } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
import { unauthorized, json } from '@/lib/response'; import { unauthorized, json } from '@/lib/response';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { getCompareDate } from '@/lib/date';
import { filterParams } from '@/lib/schema'; import { filterParams } from '@/lib/schema';
import { getWebsiteStats } from '@/queries'; import { getWebsiteStats } from '@/queries';
@ -24,32 +23,20 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { compare } = query;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
} }
const { startDate, endDate } = await getRequestDateRange(query); const filters = await getQueryFilters({ ...query, websiteId });
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
const filters = getRequestFilters(query); const data = await getWebsiteStats(websiteId, filters);
const metrics = await getWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
});
const previous = await getWebsiteStats(websiteId, { const previous = await getWebsiteStats(websiteId, {
...filters, ...filters,
startDate: compareStartDate, startDate: filters.compareStartDate,
endDate: compareEndDate, endDate: filters.compareEndDate,
}); });
return json({ ...metrics, previous }); return json({ ...data, previous });
} }

View file

@ -2,7 +2,7 @@ import { z } from 'zod';
import { canViewWebsite } from '@/lib/auth'; import { canViewWebsite } from '@/lib/auth';
import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { EVENT_COLUMNS, FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
import { getValues } from '@/queries'; import { getValues } from '@/queries';
import { parseRequest, getRequestDateRange } from '@/lib/request'; import { parseRequest, getQueryFilters } from '@/lib/request';
import { badRequest, json, unauthorized } from '@/lib/response'; import { badRequest, json, unauthorized } from '@/lib/response';
export async function GET( export async function GET(
@ -23,7 +23,7 @@ export async function GET(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { type, search } = query; const { type } = query;
if (!(await canViewWebsite(auth, websiteId))) { if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
@ -33,9 +33,9 @@ export async function GET(
return badRequest('Invalid type.'); return badRequest('Invalid type.');
} }
const { startDate, endDate } = await getRequestDateRange(query); const filters = await getQueryFilters(query);
const values = await getValues(websiteId, FILTER_COLUMNS[type], startDate, endDate, search); const values = await getValues(websiteId, FILTER_COLUMNS[type], { ...filters });
return json(values.filter(n => n).sort()); return json(values.filter(n => n).sort());
} }

View file

@ -1,14 +1,13 @@
import { ReactNode } from 'react'; import { ReactNode, useState } from 'react';
import { SearchField, Row, Column } from '@umami/react-zen'; import { SearchField, Row, Column } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { Pager } from '@/components/common/Pager'; import { Pager } from '@/components/common/Pager';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PagedQueryResult } from '@/lib/types';
const DEFAULT_SEARCH_DELAY = 600; const DEFAULT_SEARCH_DELAY = 600;
export interface DataTableProps { export interface DataTableProps {
queryResult: PagedQueryResult<any>; queryResult: any;
searchDelay?: number; searchDelay?: number;
allowSearch?: boolean; allowSearch?: boolean;
allowPaging?: boolean; allowPaging?: boolean;
@ -20,31 +19,30 @@ export interface DataTableProps {
export function DataGrid({ export function DataGrid({
queryResult, queryResult,
searchDelay = 600, searchDelay = 600,
allowSearch = true, allowSearch,
allowPaging = true, allowPaging,
autoFocus, autoFocus,
children, children,
}: DataTableProps) { }: DataTableProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { result, params, setParams, query } = queryResult || {}; const { data, error, isLoading, isFetching, setParams } = queryResult || {};
const { error, isLoading, isFetching } = query || {};
const { page, pageSize, count, data } = result || {};
const { search } = params || {};
const hasData = Boolean(!isLoading && data?.length);
const { router, updateParams } = useNavigation(); const { router, updateParams } = useNavigation();
const [search, setSearch] = useState('');
const handleSearch = (search: string) => { const handleSearch = (search: string) => {
setParams({ ...params, search }); setSearch(search);
setParams(params => ({ ...params, search }));
router.push(updateParams({ search }));
}; };
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
setParams({ ...params, page }); setParams(params => ({ ...params, page }));
router.push(updateParams({ page })); router.push(updateParams({ page }));
}; };
return ( return (
<Column gap="4" minHeight="300px"> <Column gap="4" minHeight="300px">
{allowSearch && (hasData || search) && ( {allowSearch && (data || search) && (
<Row width="280px" alignItems="center"> <Row width="280px" alignItems="center">
<SearchField <SearchField
value={search} value={search}
@ -57,11 +55,16 @@ export function DataGrid({
)} )}
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}> <LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<Column> <Column>
{hasData ? (typeof children === 'function' ? children(result) : children) : null} {data ? (typeof children === 'function' ? children(data) : children) : null}
</Column> </Column>
{allowPaging && hasData && ( {allowPaging && data && (
<Row marginTop="6"> <Row marginTop="6">
<Pager page={page} pageSize={pageSize} count={count} onPageChange={handlePageChange} /> <Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
onPageChange={handlePageChange}
/>
</Row> </Row>
)} )}
</LoadingPanel> </LoadingPanel>

View file

@ -19,7 +19,7 @@ export function FilterEditForm({ websiteId, data = [], onChange, onClose }: Filt
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange(websiteId);
const updateFilter = (name: string, props: { [key: string]: any }) => { const updateFilter = (name: string, props: Record<string, any>) => {
setFilters(filters => setFilters(filters =>
filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)), filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)),
); );

View file

@ -24,7 +24,7 @@ export * from './queries/useTeamMembersQuery';
export * from './queries/useUserQuery'; export * from './queries/useUserQuery';
export * from './queries/useUsersQuery'; export * from './queries/useUsersQuery';
export * from './queries/useWebsiteQuery'; export * from './queries/useWebsiteQuery';
export * from './queries/useWebsites'; export * from './queries/useWebsitesQuery';
export * from './queries/useWebsiteEventsQuery'; export * from './queries/useWebsiteEventsQuery';
export * from './queries/useWebsiteEventsSeriesQuery'; export * from './queries/useWebsiteEventsSeriesQuery';
export * from './queries/useWebsiteMetricsQuery'; export * from './queries/useWebsiteMetricsQuery';

View file

@ -1,6 +1,6 @@
import { useApi, useModified } from '@/components/hooks'; import { useApi, useModified } from '@/components/hooks';
export function useDeleteQuery(path: string, params?: { [key: string]: any }) { export function useDeleteQuery(path: string, params?: Record<string, any>) {
const { del, useMutation } = useApi(); const { del, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({ const { mutate, isPending, error } = useMutation({
mutationFn: () => del(path, params), mutationFn: () => del(path, params),

View file

@ -1,8 +1,27 @@
import { useTimezone } from '@/components/hooks/useTimezone'; import { useTimezone } from '@/components/hooks/useTimezone';
import { REALTIME_INTERVAL } from '@/lib/constants'; import { REALTIME_INTERVAL } from '@/lib/constants';
import { RealtimeData } from '@/lib/types';
import { useApi } from '../useApi'; import { useApi } from '../useApi';
export interface RealtimeData {
countries: Record<string, number>;
events: any[];
pageviews: any[];
referrers: Record<string, number>;
timestamp: number;
series: {
views: any[];
visitors: any[];
};
totals: {
views: number;
visitors: number;
events: number;
countries: number;
};
urls: Record<string, number>;
visitors: any[];
}
export function useRealtimeQuery(websiteId: string) { export function useRealtimeQuery(websiteId: string) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { timezone } = useTimezone(); const { timezone } = useTimezone();

View file

@ -4,7 +4,7 @@ import { ReactQueryOptions } from '@/lib/types';
export function useResultQuery<T = any>( export function useResultQuery<T = any>(
type: string, type: string,
params?: { [key: string]: any }, params?: Record<string, any>,
options?: ReactQueryOptions<T>, options?: ReactQueryOptions<T>,
) { ) {
const { websiteId } = params; const { websiteId } = params;
@ -21,7 +21,7 @@ export function useResultQuery<T = any>(
...params, ...params,
}, },
], ],
queryFn: () => post(`/reports/${type}`, { type, ...filters, ...params }), queryFn: () => post(`/reports/${type}`, { type, filters, ...params }),
enabled: !!type, enabled: !!type,
...options, ...options,
}); });

View file

@ -1,6 +1,6 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
export function useUserQuery(userId: string, options?: { [key: string]: any }) { export function useUserQuery(userId: string, options?: Record<string, any>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
return useQuery({ return useQuery({
queryKey: ['users', userId], queryKey: ['users', userId],

View file

@ -5,11 +5,11 @@ import { ReactQueryOptions } from '@/lib/types';
export function useWebsiteEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) { export function useWebsiteEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) {
const { get } = useApi(); const { get } = useApi();
const filterParams = useFilterParams(websiteId); const queryParams = useFilterParams(websiteId);
return usePagedQuery({ return usePagedQuery({
queryKey: ['websites:events', { websiteId, ...filterParams }], queryKey: ['websites:events', { websiteId, ...queryParams }],
queryFn: () => get(`/websites/${websiteId}/events`, { ...filterParams, pageSize: 20 }), queryFn: () => get(`/websites/${websiteId}/events`, { ...queryParams, pageSize: 20 }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
options?: ReactQueryOptions<WebsiteMetricsData>, options?: ReactQueryOptions<WebsiteMetricsData>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId); const queryParams = useFilterParams(websiteId);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
return useQuery<WebsiteMetricsData>({ return useQuery<WebsiteMetricsData>({
@ -23,13 +23,13 @@ export function useWebsiteMetricsQuery(
'websites:metrics', 'websites:metrics',
{ {
websiteId, websiteId,
...filterParams, ...queryParams,
...params, ...params,
}, },
], ],
queryFn: async () => queryFn: async () =>
get(`/websites/${websiteId}/metrics`, { get(`/websites/${websiteId}/metrics`, {
...filterParams, ...queryParams,
[searchParams.get('view')]: undefined, [searchParams.get('view')]: undefined,
...params, ...params,
}), }),

View file

@ -12,11 +12,11 @@ export function useWebsitePageviewsQuery(
options?: ReactQueryOptions<WebsitePageviewsData>, options?: ReactQueryOptions<WebsitePageviewsData>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId); const queryParams = useFilterParams(websiteId);
return useQuery<WebsitePageviewsData>({ return useQuery<WebsitePageviewsData>({
queryKey: ['websites:pageviews', { websiteId, compare, ...filterParams }], queryKey: ['websites:pageviews', { websiteId, compare, ...queryParams }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...filterParams }), queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...queryParams }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -1,6 +1,6 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
export function useWebsiteQuery(websiteId: string, options?: { [key: string]: any }) { export function useWebsiteQuery(websiteId: string, options?: Record<string, any>) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
return useQuery({ return useQuery({

View file

@ -1,10 +1,7 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams'; import { useFilterParams } from '../useFilterParams';
export function useWebsiteSessionStatsQuery( export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record<string, string>) {
websiteId: string,
options?: { [key: string]: string },
) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const params = useFilterParams(websiteId);

View file

@ -5,7 +5,7 @@ import { useFilterParams } from '@/components/hooks/useFilterParams';
export function useWebsiteSessionsQuery( export function useWebsiteSessionsQuery(
websiteId: string, websiteId: string,
params?: { [key: string]: string | number }, params?: Record<string, string | number>,
) { ) {
const { get } = useApi(); const { get } = useApi();
const { modified } = useModified(`sessions`); const { modified } = useModified(`sessions`);

View file

@ -4,7 +4,7 @@ import { useFilterParams } from '@/components/hooks/useFilterParams';
export function useWebsiteSessionsWeeklyQuery( export function useWebsiteSessionsWeeklyQuery(
websiteId: string, websiteId: string,
params?: { [key: string]: string | number }, params?: Record<string, string | number>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const { modified } = useModified(`sessions`); const { modified } = useModified(`sessions`);

View file

@ -19,15 +19,14 @@ export interface WebsiteStatsData {
export function useWebsiteStatsQuery( export function useWebsiteStatsQuery(
websiteId: string, websiteId: string,
compare?: string,
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>, options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
) { ) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
const params = useFilterParams(websiteId); const filterParams = useFilterParams(websiteId);
return useQuery<WebsiteStatsData>({ return useQuery<WebsiteStatsData>({
queryKey: ['websites:stats', { websiteId, ...params, compare }], queryKey: ['websites:stats', { websiteId, ...filterParams }],
queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }), queryFn: () => get(`/websites/${websiteId}/stats`, { ...filterParams }),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });

View file

@ -2,10 +2,12 @@ import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery'; import { usePagedQuery } from '../usePagedQuery';
import { useLoginQuery } from './useLoginQuery'; import { useLoginQuery } from './useLoginQuery';
import { useModified } from '../useModified'; import { useModified } from '../useModified';
import { ReactQueryOptions } from '@/lib/types';
export function useWebsites( export function useWebsitesQuery(
{ userId, teamId }: { userId?: string; teamId?: string }, { userId, teamId }: { userId?: string; teamId?: string },
params?: { [key: string]: string | number }, params?: Record<string, any>,
options?: ReactQueryOptions,
) { ) {
const { get } = useApi(); const { get } = useApi();
const { user } = useLoginQuery(); const { user } = useLoginQuery();
@ -13,10 +15,12 @@ export function useWebsites(
return usePagedQuery({ return usePagedQuery({
queryKey: ['websites', { userId, teamId, modified, ...params }], queryKey: ['websites', { userId, teamId, modified, ...params }],
queryFn: () => { queryFn: pageParams => {
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, { return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
...params, ...params,
...pageParams,
}); });
}, },
...options,
}); });
} }

View file

@ -40,7 +40,7 @@ export function useFormat() {
return languageNames[value?.split('-')[0]] || value; return languageNames[value?.split('-')[0]] || value;
}; };
const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => { const formatValue = (value: string, type: string, data?: Record<string, any>): string => {
switch (type) { switch (type) {
case 'os': case 'os':
return formatOS(value); return formatOS(value);

View file

@ -15,7 +15,7 @@ export function useMessages(): any {
id: string; id: string;
defaultMessage: string; defaultMessage: string;
}, },
values?: { [key: string]: string }, values?: Record<string, string>,
opts?: any, opts?: any,
) => { ) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null; return descriptor ? intl.formatMessage(descriptor, values, opts) : null;

View file

@ -9,11 +9,11 @@ export function useNavigation() {
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || []; const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
const query = Object.fromEntries(searchParams); const query = Object.fromEntries(searchParams);
const updateParams = (params?: { [key: string]: string | number }) => { const updateParams = (params?: Record<string, string | number>) => {
return !params ? pathname : buildUrl(pathname, { ...query, ...params }); return !params ? pathname : buildUrl(pathname, { ...query, ...params });
}; };
const renderUrl = (path: string, params?: { [key: string]: string | number } | false) => { const renderUrl = (path: string, params?: Record<string, string | number> | false) => {
return buildUrl( return buildUrl(
teamId ? `/teams/${teamId}${path}` : path, teamId ? `/teams/${teamId}${path}` : path,
params === false ? {} : { ...query, ...params }, params === false ? {} : { ...query, ...params },

View file

@ -1,30 +1,27 @@
import { UseQueryOptions } from '@tanstack/react-query'; import { UseQueryOptions } from '@tanstack/react-query';
import { useState } from 'react'; import { useState } from 'react';
import { PageResult, PageParams, PagedQueryResult } from '@/lib/types';
import { useApi } from './useApi'; import { useApi } from './useApi';
import { useNavigation } from './useNavigation'; import { useNavigation } from './useNavigation';
export function usePagedQuery<T = any>({ export function usePagedQuery({
queryKey, queryKey,
queryFn, queryFn,
...options ...options
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): PagedQueryResult<T> { }: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }) {
const { query: queryParams } = useNavigation(); const { query: queryParams } = useNavigation();
const [params, setParams] = useState<PageParams>({ const [params, setParams] = useState({
search: '', search: queryParams?.search ?? '',
page: queryParams?.page || '1', page: queryParams?.page ?? '1',
}); });
const { useQuery } = useApi(); const { useQuery } = useApi();
const { data, ...query } = useQuery({
queryKey: [{ ...queryKey, ...params }],
queryFn: () => queryFn(params as any),
...options,
});
return { return {
result: data as PageResult<T>, ...useQuery({
query, queryKey: [{ ...queryKey, ...params }],
queryFn: () => queryFn(params),
...options,
}),
params, params,
setParams, setParams,
}; };

View file

@ -1,7 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Select, ListItem } from '@umami/react-zen'; import { Select, SelectProps, ListItem } from '@umami/react-zen';
import { useWebsites, useMessages } from '@/components/hooks'; import { useWebsitesQuery, useMessages } from '@/components/hooks';
import type { SelectProps } from '@umami/react-zen/Select';
export function WebsiteSelect({ export function WebsiteSelect({
websiteId, websiteId,
@ -12,14 +11,14 @@ export function WebsiteSelect({
}: { }: {
websiteId?: string; websiteId?: string;
teamId?: string; teamId?: string;
variant?: 'primary' | 'secondary' | 'outline' | 'quiet' | 'danger' | 'zero'; variant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
onSelect?: (key: any) => void; onSelect?: (key: any) => void;
} & SelectProps) { } & SelectProps) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedId, setSelectedId] = useState(websiteId); const [selectedId, setSelectedId] = useState(websiteId);
const queryResult = useWebsites({ teamId }, { search, pageSize: 5 }); const { data, isLoading } = useWebsitesQuery({ teamId }, { search, pageSize: 5 });
const handleSelect = (value: any) => { const handleSelect = (value: any) => {
setSelectedId(value); setSelectedId(value);
@ -30,15 +29,17 @@ export function WebsiteSelect({
setSearch(value); setSearch(value);
}; };
const items = queryResult?.result?.data as any[]; if (!data) {
return null;
}
return ( return (
<Select <Select
{...props} {...props}
items={items} items={data?.['data'] || []}
value={selectedId} value={selectedId}
placeholder={formatMessage(labels.selectWebsite)} placeholder={formatMessage(labels.selectWebsite)}
isLoading={queryResult.query.isLoading} isLoading={isLoading}
buttonProps={{ variant }} buttonProps={{ variant }}
allowSearch={true} allowSearch={true}
searchValue={search} searchValue={search}

View file

@ -13,7 +13,6 @@ export interface MetricCardProps {
formatValue?: (n: any) => string; formatValue?: (n: any) => string;
showLabel?: boolean; showLabel?: boolean;
showChange?: boolean; showChange?: boolean;
showPrevious?: boolean;
} }
export const MetricCard = ({ export const MetricCard = ({
@ -24,13 +23,11 @@ export const MetricCard = ({
formatValue = formatNumber, formatValue = formatNumber,
showLabel = true, showLabel = true,
showChange = false, showChange = false,
showPrevious = false,
}: MetricCardProps) => { }: MetricCardProps) => {
const diff = value - change; const diff = value - change;
const pct = ((value - diff) / diff) * 100; const pct = ((value - diff) / diff) * 100;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } }); const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } }); const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
return ( return (
<Column <Column
@ -54,9 +51,6 @@ export const MetricCard = ({
<AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv> <AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv>
</ChangeLabel> </ChangeLabel>
)} )}
{showPrevious && (
<AnimatedDiv title={diff.toString()}>{prevProps?.x?.to(x => formatValue(x))}</AnimatedDiv>
)}
</Column> </Column>
); );
}; };

View file

@ -18,7 +18,7 @@ export interface MetricsTableProps extends ListTableProps {
allowSearch?: boolean; allowSearch?: boolean;
searchFormattedValues?: boolean; searchFormattedValues?: boolean;
showMore?: boolean; showMore?: boolean;
params?: { [key: string]: any }; params?: Record<string, any>;
onDataLoad?: (data: any) => any; onDataLoad?: (data: any) => any;
className?: string; className?: string;
children?: ReactNode; children?: ReactNode;

View file

@ -2,11 +2,9 @@ import { ClickHouseClient, createClient } from '@clickhouse/client';
import { formatInTimeZone } from 'date-fns-tz'; import { formatInTimeZone } from 'date-fns-tz';
import debug from 'debug'; import debug from 'debug';
import { CLICKHOUSE } from '@/lib/db'; import { CLICKHOUSE } from '@/lib/db';
import { getWebsite } from '@/queries';
import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants'; import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
import { maxDate } from './date';
import { filtersToArray } from './params'; import { filtersToArray } from './params';
import { PageParams, QueryFilters, QueryOptions } from './types'; import { QueryFilters, QueryOptions } from './types';
export const CLICKHOUSE_DATE_FORMATS = { export const CLICKHOUSE_DATE_FORMATS = {
utc: '%Y-%m-%dT%H:%i:%SZ', utc: '%Y-%m-%dT%H:%i:%SZ',
@ -89,7 +87,7 @@ function mapFilter(column: string, operator: string, name: string, type: string
} }
} }
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}) { function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}) {
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => { const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
if (column) { if (column) {
arr.push(`and ${mapFilter(column, operator, name)}`); arr.push(`and ${mapFilter(column, operator, name)}`);
@ -105,7 +103,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
return query.join('\n'); return query.join('\n');
} }
function getDateQuery(filters: QueryFilters = {}) { function getDateQuery(filters: Record<string, any>) {
const { startDate, endDate, timezone } = filters; const { startDate, endDate, timezone } = filters;
if (startDate) { if (startDate) {
@ -125,36 +123,33 @@ function getDateQuery(filters: QueryFilters = {}) {
return ''; return '';
} }
function getFilterParams(filters: QueryFilters = {}) { function getQueryParams(filters: Record<string, any>) {
return filtersToArray(filters).reduce((obj, { name, value }) => {
if (name && value !== undefined) {
obj[name] = value;
}
return obj;
}, {});
}
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
const website = await getWebsite(websiteId);
return { return {
filterQuery: getFilterQuery(filters, options), ...filters,
dateQuery: getDateQuery(filters), ...filtersToArray(filters).reduce((obj, { name, value }) => {
filterParams: { if (name && value !== undefined) {
...getFilterParams(filters), obj[name] = value;
websiteId, }
startDate: maxDate(filters.startDate, new Date(website?.resetAt)),
}, return obj;
}, {}),
}; };
} }
async function pagedQuery( async function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
return {
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters),
queryParams: getQueryParams(filters),
};
}
async function pagedRawQuery(
query: string, query: string,
queryParams: { [key: string]: any }, queryParams: Record<string, any>,
pageParams: PageParams = {}, filters: QueryFilters,
) { ) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams; const { page = 1, pageSize, orderBy, sortDescending = false } = filters;
const size = +pageSize || DEFAULT_PAGE_SIZE; const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (+page - 1); const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc'; const direction = sortDescending ? 'desc' : 'asc';
@ -236,7 +231,7 @@ export default {
getFilterQuery, getFilterQuery,
getUTCString, getUTCString,
parseFilters, parseFilters,
pagedQuery, pagedRawQuery,
findUnique, findUnique,
findFirst, findFirst,
rawQuery, rawQuery,

View file

@ -19,7 +19,7 @@ export const DEFAULT_ANIMATION_DURATION = 300;
export const DEFAULT_DATE_RANGE_VALUE = '24hour'; export const DEFAULT_DATE_RANGE_VALUE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10; export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01'; export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 10; export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_DATE_COMPARE = 'prev'; export const DEFAULT_DATE_COMPARE = 'prev';
export const REALTIME_RANGE = 30; export const REALTIME_RANGE = 30;

View file

@ -2,7 +2,7 @@ import { DATA_TYPE, DATETIME_REGEX } from './constants';
import { DynamicDataType } from './types'; import { DynamicDataType } from './types';
export function flattenJSON( export function flattenJSON(
eventData: { [key: string]: any }, eventData: Record<string, any>,
keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [], keyValues: { key: string; value: any; dataType: DynamicDataType }[] = [],
parentKey = '', parentKey = '',
): { key: string; value: any; dataType: DynamicDataType }[] { ): { key: string; value: any; dataType: DynamicDataType }[] {

View file

@ -292,9 +292,13 @@ export function getCompareDate(compare: string, startDate: Date, endDate: Date)
return { startDate: subYears(startDate, 1), endDate: subYears(endDate, 1) }; return { startDate: subYears(startDate, 1), endDate: subYears(endDate, 1) };
} }
const diff = differenceInMinutes(endDate, startDate); if (compare === 'prev') {
const diff = differenceInMinutes(endDate, startDate);
return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) }; return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) };
}
return {};
} }
export function getDayOfWeekAsDate(dayOfWeek: number) { export function getDayOfWeekAsDate(dayOfWeek: number) {

View file

@ -64,7 +64,7 @@ async function getProducer(): Promise<Producer> {
async function sendMessage( async function sendMessage(
topic: string, topic: string,
message: { [key: string]: string | number } | { [key: string]: string | number }[], message: Record<string, string | number> | Record<string, string | number>[],
): Promise<RecordMetadata[]> { ): Promise<RecordMetadata[]> {
try { try {
await connect(); await connect();

View file

@ -18,7 +18,7 @@ export function isSearchOperator(operator: any) {
return [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator); return [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator);
} }
export function filtersToArray(filters: QueryFilters = {}, options: QueryOptions = {}) { export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}) {
return Object.keys(filters).reduce((arr, key) => { return Object.keys(filters).reduce((arr, key) => {
const filter = filters[key]; const filter = filters[key];

View file

@ -2,12 +2,9 @@ import debug from 'debug';
import { PrismaClient } from '@/generated/prisma/client'; import { PrismaClient } from '@/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaPg } from '@prisma/adapter-pg';
import { readReplicas } from '@prisma/extension-read-replicas'; import { readReplicas } from '@prisma/extension-read-replicas';
import { formatInTimeZone } from 'date-fns-tz';
import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db'; import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
import { fetchWebsite } from './load'; import { QueryOptions, QueryFilters } from './types';
import { maxDate } from './date';
import { QueryFilters, QueryOptions, PageParams } from './types';
import { filtersToArray } from './params'; import { filtersToArray } from './params';
const log = debug('umami:prisma'); const log = debug('umami:prisma');
@ -22,14 +19,6 @@ const PRISMA_LOG_OPTIONS = {
], ],
}; };
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%dT%H:%i:00',
hour: '%Y-%m-%d %H:00:00',
day: '%Y-%m-%d 00:00:00',
month: '%Y-%m-01 00:00:00',
year: '%Y-01-01 00:00:00',
};
const POSTGRESQL_DATE_FORMATS = { const POSTGRESQL_DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00', minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00', hour: 'YYYY-MM-DD HH24:00:00',
@ -75,59 +64,23 @@ function getCastColumnQuery(field: string, type: string): string {
} }
function getDateSQL(field: string, unit: string, timezone?: string): string { function getDateSQL(field: string, unit: string, timezone?: string): string {
const db = getDatabaseType(); if (timezone) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
if (db === POSTGRESQL) {
if (timezone) {
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
}
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
} }
if (db === MYSQL) { return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
if (timezone) {
const tz = formatInTimeZone(new Date(), timezone, 'xxx');
return `date_format(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
}
return `date_format(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
}
} }
function getDateWeeklySQL(field: string, timezone?: string) { function getDateWeeklySQL(field: string, timezone?: string) {
const db = getDatabaseType(); return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`;
if (db === POSTGRESQL) {
return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`;
}
if (db === MYSQL) {
const tz = formatInTimeZone(new Date(), timezone, 'xxx');
return `date_format(convert_tz(${field},'+00:00','${tz}'), '%w:%H')`;
}
} }
export function getTimestampSQL(field: string) { export function getTimestampSQL(field: string) {
const db = getDatabaseType(); return `floor(extract(epoch from ${field}))`;
if (db === POSTGRESQL) {
return `floor(extract(epoch from ${field}))`;
}
if (db === MYSQL) {
return `UNIX_TIMESTAMP(${field})`;
}
} }
function getTimestampDiffSQL(field1: string, field2: string): string { function getTimestampDiffSQL(field1: string, field2: string): string {
const db = getDatabaseType(); return `floor(extract(epoch from (${field2} - ${field1})))`;
if (db === POSTGRESQL) {
return `floor(extract(epoch from (${field2} - ${field1})))`;
}
if (db === MYSQL) {
return `timestampdiff(second, ${field1}, ${field2})`;
}
} }
function getSearchSQL(column: string, param: string = 'search'): string { function getSearchSQL(column: string, param: string = 'search'): string {
@ -156,7 +109,7 @@ function mapFilter(column: string, operator: string, name: string, type: string
} }
} }
function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}): string { function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => { const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
if (column) { if (column) {
arr.push(`and ${mapFilter(column, operator, name)}`); arr.push(`and ${mapFilter(column, operator, name)}`);
@ -174,7 +127,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
return query.join('\n'); return query.join('\n');
} }
function getDateQuery(filters: QueryFilters = {}) { function getDateQuery(filters: Record<string, any>) {
const { startDate, endDate } = filters; const { startDate, endDate } = filters;
if (startDate) { if (startDate) {
@ -188,38 +141,32 @@ function getDateQuery(filters: QueryFilters = {}) {
return ''; return '';
} }
function getFilterParams(filters: QueryFilters = {}) { function getQueryParams(filters: Record<string, any>) {
return filtersToArray(filters).reduce((obj, { name, operator, value }) => { return {
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator) ...filters,
? `%${value}%` ...filtersToArray(filters).reduce((obj, { name, operator, value }) => {
: value; obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
? `%${value}%`
: value;
return obj; return obj;
}, {}); }, {}),
};
} }
async function parseFilters( async function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
websiteId: string,
filters: QueryFilters = {},
options: QueryOptions = {},
) {
const website = await fetchWebsite(websiteId);
const joinSession = Object.keys(filters).find(key => const joinSession = Object.keys(filters).find(key =>
['referrer', ...SESSION_COLUMNS].includes(key), ['referrer', ...SESSION_COLUMNS].includes(key),
); );
return { return {
joinSession: joinSessionQuery:
options?.joinSession || joinSession options?.joinSession || joinSession
? `inner join session on website_event.session_id = session.session_id` ? `inner join session on website_event.session_id = session.session_id`
: '', : '',
filterQuery: getFilterQuery(filters, options),
dateQuery: getDateQuery(filters), dateQuery: getDateQuery(filters),
filterParams: { filterQuery: getFilterQuery(filters, options),
...getFilterParams(filters), queryParams: getQueryParams(filters),
websiteId,
startDate: maxDate(filters.startDate, website?.resetAt),
},
}; };
} }
@ -251,8 +198,8 @@ async function rawQuery(sql: string, data: object): Promise<any> {
: client.$queryRawUnsafe(query, ...params); : client.$queryRawUnsafe(query, ...params);
} }
async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) { async function pagedQuery<T>(model: string, criteria: T, filters?: QueryFilters) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {}; const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
const size = +pageSize || DEFAULT_PAGE_SIZE; const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await client[model].findMany({ const data = await client[model].findMany({
@ -276,10 +223,10 @@ async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams)
async function pagedRawQuery( async function pagedRawQuery(
query: string, query: string,
queryParams: { [key: string]: any }, filters: QueryFilters,
pageParams: PageParams = {}, queryParams: Record<string, any>,
) { ) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams; const { page = 1, pageSize, orderBy, sortDescending = false } = filters;
const size = +pageSize || DEFAULT_PAGE_SIZE; const size = +pageSize || DEFAULT_PAGE_SIZE;
const offset = +size * (+page - 1); const offset = +size * (+page - 1);
const direction = sortDescending ? 'desc' : 'asc'; const direction = sortDescending ? 'desc' : 'asc';
@ -310,11 +257,11 @@ function getQueryMode(): { mode?: 'default' | 'insensitive' } {
return {}; return {};
} }
function getSearchParameters(query: string, filters: { [key: string]: any }[]) { function getSearchParameters(query: string, filters: Record<string, any>[]) {
if (!query) return; if (!query) return;
const mode = getQueryMode(); const mode = getQueryMode();
const parseFilter = (filter: { [key: string]: any }) => { const parseFilter = (filter: Record<string, any>) => {
const [[key, value]] = Object.entries(filter); const [[key, value]] = Object.entries(filter);
return { return {

View file

@ -1,8 +1,10 @@
import { z } from 'zod/v4'; import { z } from 'zod/v4';
import { FILTER_COLUMNS } from '@/lib/constants'; import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants';
import { badRequest, unauthorized } from '@/lib/response'; import { badRequest, unauthorized } from '@/lib/response';
import { getAllowedUnits, getCompareDate, getMinimumUnit } from '@/lib/date'; import { getAllowedUnits, getCompareDate, getMinimumUnit, maxDate } from '@/lib/date';
import { checkAuth } from '@/lib/auth'; import { checkAuth } from '@/lib/auth';
import { fetchWebsite } from '@/lib/load';
import { QueryFilters } from '@/lib/types';
export async function parseRequest( export async function parseRequest(
request: Request, request: Request,
@ -47,8 +49,8 @@ export async function getJsonBody(request: Request) {
} }
} }
export async function getRequestDateRange(query: Record<string, string>) { export function getRequestDateRange(query: Record<string, string>) {
const { startAt, endAt, unit, compare } = query; const { startAt, endAt, unit, compare, timezone } = query;
const startDate = new Date(+startAt); const startDate = new Date(+startAt);
const endDate = new Date(+endAt); const endDate = new Date(+endAt);
@ -62,8 +64,10 @@ export async function getRequestDateRange(query: Record<string, string>) {
return { return {
startDate, startDate,
endDate, endDate,
compare,
compareStartDate, compareStartDate,
compareEndDate, compareEndDate,
timezone,
unit: getAllowedUnits(startDate, endDate).includes(unit) unit: getAllowedUnits(startDate, endDate).includes(unit)
? unit ? unit
: getMinimumUnit(startDate, endDate), : getMinimumUnit(startDate, endDate),
@ -81,3 +85,30 @@ export function getRequestFilters(query: Record<string, any>) {
return obj; return obj;
}, {}); }, {});
} }
export async function getQueryFilters(params: Record<string, any>): Promise<QueryFilters> {
const dateRange = getRequestDateRange(params);
const filters = getRequestFilters(params);
const data = {
...dateRange,
...filters,
page: params?.page,
pageSize: params?.page ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
orderBy: params?.orderBy,
sortDescending: params?.sortDescending,
search: params?.search,
websiteId: undefined,
};
const { websiteId } = params;
if (websiteId) {
const website = await fetchWebsite(websiteId);
data.websiteId = websiteId;
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
}
return data;
}

View file

@ -2,6 +2,22 @@ import { z } from 'zod';
import { isValidTimezone } from '@/lib/date'; import { isValidTimezone } from '@/lib/date';
import { UNIT_TYPES } from './constants'; import { UNIT_TYPES } from './constants';
export const timezoneParam = z.string().refine(value => isValidTimezone(value), {
message: 'Invalid timezone',
});
export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
message: 'Invalid unit',
});
export const dateRangeParams = {
startAt: z.coerce.number(),
endAt: z.coerce.number(),
timezone: timezoneParam.optional(),
unit: unitParam.optional(),
compare: z.string().optional(),
};
export const filterParams = { export const filterParams = {
path: z.string().optional(), path: z.string().optional(),
referrer: z.string().optional(), referrer: z.string().optional(),
@ -22,17 +38,13 @@ export const filterParams = {
export const pagingParams = { export const pagingParams = {
page: z.coerce.number().int().positive().optional(), page: z.coerce.number().int().positive().optional(),
pageSize: z.coerce.number().int().positive().optional(), pageSize: z.coerce.number().int().positive().optional(),
orderBy: z.string().optional(),
search: z.string().optional(), search: z.string().optional(),
}; };
export const timezoneParam = z.string().refine(value => isValidTimezone(value), { export const sortingParams = {
message: 'Invalid timezone', orderBy: z.string().optional(),
}); sortDescending: z.string().optional(),
};
export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), {
message: 'Invalid unit',
});
export const userRoleParam = z.enum(['admin', 'user', 'view-only']); export const userRoleParam = z.enum(['admin', 'user', 'view-only']);
@ -86,11 +98,11 @@ export const reportParms = {
dateRange: z.object({ dateRange: z.object({
startDate: z.coerce.date(), startDate: z.coerce.date(),
endDate: z.coerce.date(), endDate: z.coerce.date(),
num: z.coerce.number().optional(),
offset: z.coerce.number().optional(),
timezone: timezoneParam.optional(), timezone: timezoneParam.optional(),
unit: unitParam.optional(), unit: unitParam.optional(),
value: z.string().optional(), compare: z.string().optional(),
compareStartDate: z.coerce.date().optional(),
compareEndDate: z.coerce.date().optional(),
}), }),
}; };
@ -191,7 +203,7 @@ export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);
export const reportResultSchema = z.intersection( export const reportResultSchema = z.intersection(
z.object({ z.object({
...reportParms, ...reportParms,
...filterParams, filters: z.object({ ...filterParams }),
}), }),
reportTypeSchema, reportTypeSchema,
); );

View file

@ -1,45 +1,16 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryOptions } from '@tanstack/react-query'; import { UseQueryOptions } from '@tanstack/react-query';
import { DATA_TYPE, PERMISSIONS, ROLES } from './constants'; import { DATA_TYPE, PERMISSIONS, ROLES } from './constants';
import { TIME_UNIT } from './date'; import { TIME_UNIT } from './date';
export type ObjectValues<T> = T[keyof T]; export type ObjectValues<T> = T[keyof T];
export type ReactQueryOptions<T> = Omit<UseQueryOptions<T, Error, T>, 'queryKey' | 'queryFn'>; export type ReactQueryOptions<T = any> = Omit<UseQueryOptions<T, Error, T>, 'queryKey' | 'queryFn'>;
export type TimeUnit = ObjectValues<typeof TIME_UNIT>; export type TimeUnit = ObjectValues<typeof TIME_UNIT>;
export type Permission = ObjectValues<typeof PERMISSIONS>; export type Permission = ObjectValues<typeof PERMISSIONS>;
export type Role = ObjectValues<typeof ROLES>; export type Role = ObjectValues<typeof ROLES>;
export type DynamicDataType = ObjectValues<typeof DATA_TYPE>; export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
export interface PageParams {
search?: string;
page?: string | number;
pageSize?: string;
orderBy?: string;
sortDescending?: boolean;
}
export interface PageResult<T> {
data: T;
count: number;
page: number;
pageSize: number;
orderBy?: string;
sortDescending?: boolean;
}
export interface PagedQueryResult<T = any> {
result: PageResult<T>;
query: any;
params: PageParams;
setParams: Dispatch<SetStateAction<T | PageParams>>;
}
export interface DynamicData {
[key: string]: number | string | number[] | string[];
}
export interface Auth { export interface Auth {
user?: { user?: {
id: string; id: string;
@ -54,20 +25,35 @@ export interface Auth {
} }
export interface DateRange { export interface DateRange {
value: string;
startDate: Date; startDate: Date;
endDate: Date; endDate: Date;
value?: string;
unit?: TimeUnit; unit?: TimeUnit;
num?: number; num?: number;
offset?: number; offset?: number;
} }
export interface DynamicData {
[key: string]: number | string | number[] | string[];
}
export interface QueryOptions {
joinSession?: boolean;
columns?: Record<string, string>;
limit?: number;
}
export interface QueryFilters { export interface QueryFilters {
websiteId?: string;
// Date range
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
timezone?: string; compareStartDate?: Date;
compareEndDate?: Date;
compare?: string;
unit?: string; unit?: string;
eventType?: number; timezone?: string;
// Filters
path?: string; path?: string;
referrer?: string; referrer?: string;
title?: string; title?: string;
@ -83,54 +69,29 @@ export interface QueryFilters {
event?: string; event?: string;
search?: string; search?: string;
tag?: string; tag?: string;
eventType?: number;
// Paging
page?: number;
pageSize?: number;
// Sorting
orderBy?: string;
sortDescending?: boolean;
} }
export interface QueryOptions { export interface PageParams {
joinSession?: boolean; page: number;
columns?: { [key: string]: string }; pageSize: number;
limit?: number; orderBy?: string;
sortDescending?: boolean;
search?: string;
} }
export interface RealtimeData { export interface PageResult<T> {
countries: { [key: string]: number }; data: T;
events: any[]; count: number;
pageviews: any[]; page: number;
referrers: { [key: string]: number }; pageSize: number;
timestamp: number; orderBy?: string;
series: { sortDescending?: boolean;
views: any[]; search?: string;
visitors: any[];
};
totals: {
views: number;
visitors: number;
events: number;
countries: number;
};
urls: { [key: string]: number };
visitors: any[];
}
export interface SessionData {
id: string;
websiteId: string;
visitId: string;
hostname: string;
browser: string;
os: string;
device: string;
screen: string;
language: string;
country: string;
region: string;
city: string;
ip?: string;
userAgent?: string;
}
export interface InputItem {
id: string;
label: string;
icon: any;
seperator?: boolean;
} }

View file

@ -1,6 +1,6 @@
import { Prisma, Report } from '@/generated/prisma/client'; import { Prisma, Report } from '@/generated/prisma/client';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types'; import { PageResult, QueryFilters } from '@/lib/types';
import ReportFindManyArgs = Prisma.ReportFindManyArgs; import ReportFindManyArgs = Prisma.ReportFindManyArgs;
async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> { async function findReport(criteria: Prisma.ReportFindUniqueArgs): Promise<Report> {
@ -17,9 +17,9 @@ export async function getReport(reportId: string): Promise<Report> {
export async function getReports( export async function getReports(
criteria: ReportFindManyArgs, criteria: ReportFindManyArgs,
pageParams: PageParams = {}, filters: QueryFilters = {},
): Promise<PageResult<Report[]>> { ): Promise<PageResult<Report[]>> {
const { search } = pageParams; const { search } = filters;
const where: Prisma.ReportWhereInput = { const where: Prisma.ReportWhereInput = {
...criteria.where, ...criteria.where,
@ -45,12 +45,12 @@ export async function getReports(
]), ]),
}; };
return prisma.pagedQuery('report', { ...criteria, where }, pageParams); return prisma.pagedQuery('report', { ...criteria, where }, filters);
} }
export async function getUserReports( export async function getUserReports(
userId: string, userId: string,
filters?: PageParams, filters?: QueryFilters,
): Promise<PageResult<Report[]>> { ): Promise<PageResult<Report[]>> {
return getReports( return getReports(
{ {
@ -72,7 +72,7 @@ export async function getUserReports(
export async function getWebsiteReports( export async function getWebsiteReports(
websiteId: string, websiteId: string,
filters: PageParams = {}, filters: QueryFilters = {},
): Promise<PageResult<Report[]>> { ): Promise<PageResult<Report[]>> {
return getReports( return getReports(
{ {

View file

@ -2,7 +2,7 @@ import { Prisma, Team } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { uuid } from '@/lib/crypto'; import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types'; import { PageResult, QueryFilters } from '@/lib/types';
import TeamFindManyArgs = Prisma.TeamFindManyArgs; import TeamFindManyArgs = Prisma.TeamFindManyArgs;
export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> { export async function findTeam(criteria: Prisma.TeamFindUniqueArgs): Promise<Team> {
@ -22,7 +22,7 @@ export async function getTeam(teamId: string, options: { includeMembers?: boolea
export async function getTeams( export async function getTeams(
criteria: TeamFindManyArgs, criteria: TeamFindManyArgs,
filters: PageParams = {}, filters: QueryFilters,
): Promise<PageResult<Team[]>> { ): Promise<PageResult<Team[]>> {
const { getSearchParameters } = prisma; const { getSearchParameters } = prisma;
const { search } = filters; const { search } = filters;
@ -42,7 +42,7 @@ export async function getTeams(
); );
} }
export async function getUserTeams(userId: string, filters: PageParams = {}) { export async function getUserTeams(userId: string, filters: QueryFilters) {
return getTeams( return getTeams(
{ {
where: { where: {

View file

@ -1,7 +1,7 @@
import { Prisma, TeamUser } from '@/generated/prisma/client'; import { Prisma, TeamUser } from '@/generated/prisma/client';
import { uuid } from '@/lib/crypto'; import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types'; import { PageResult, QueryFilters } from '@/lib/types';
import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs; import TeamUserFindManyArgs = Prisma.TeamUserFindManyArgs;
export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> { export async function findTeamUser(criteria: Prisma.TeamUserFindUniqueArgs): Promise<TeamUser> {
@ -19,7 +19,7 @@ export async function getTeamUser(teamId: string, userId: string): Promise<TeamU
export async function getTeamUsers( export async function getTeamUsers(
criteria: TeamUserFindManyArgs, criteria: TeamUserFindManyArgs,
filters?: PageParams, filters?: QueryFilters,
): Promise<PageResult<TeamUser[]>> { ): Promise<PageResult<TeamUser[]>> {
const { search } = filters; const { search } = filters;

View file

@ -1,7 +1,7 @@
import { Prisma, User } from '@/generated/prisma/client'; import { Prisma, User } from '@/generated/prisma/client';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { PageResult, Role, PageParams } from '@/lib/types'; import { PageResult, Role, QueryFilters } from '@/lib/types';
import { getRandomChars } from '@/lib/crypto'; import { getRandomChars } from '@/lib/crypto';
import UserFindManyArgs = Prisma.UserFindManyArgs; import UserFindManyArgs = Prisma.UserFindManyArgs;
@ -49,9 +49,9 @@ export async function getUserByUsername(username: string, options: GetUserOption
export async function getUsers( export async function getUsers(
criteria: UserFindManyArgs, criteria: UserFindManyArgs,
pageParams?: PageParams, filters: QueryFilters = {},
): Promise<PageResult<User[]>> { ): Promise<PageResult<User[]>> {
const { search } = pageParams; const { search } = filters;
const where: Prisma.UserWhereInput = { const where: Prisma.UserWhereInput = {
...criteria.where, ...criteria.where,
@ -68,7 +68,7 @@ export async function getUsers(
{ {
orderBy: 'createdAt', orderBy: 'createdAt',
sortDescending: true, sortDescending: true,
...pageParams, ...filters,
}, },
); );
} }

View file

@ -1,7 +1,7 @@
import { Prisma, Website } from '@/generated/prisma/client'; import { Prisma, Website } from '@/generated/prisma/client';
import redis from '@/lib/redis'; import redis from '@/lib/redis';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { PageResult, PageParams } from '@/lib/types'; import { PageResult, QueryFilters } from '@/lib/types';
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs; import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
@ -28,9 +28,9 @@ export async function getSharedWebsite(shareId: string) {
export async function getWebsites( export async function getWebsites(
criteria: WebsiteFindManyArgs, criteria: WebsiteFindManyArgs,
pageParams: PageParams, filters: QueryFilters,
): Promise<PageResult<Website[]>> { ): Promise<PageResult<Website[]>> {
const { search } = pageParams; const { search } = filters;
const where: Prisma.WebsiteWhereInput = { const where: Prisma.WebsiteWhereInput = {
...criteria.where, ...criteria.where,
@ -43,7 +43,7 @@ export async function getWebsites(
deletedAt: null, deletedAt: null,
}; };
return prisma.pagedQuery('website', { ...criteria, where }, pageParams); return prisma.pagedQuery('website', { ...criteria, where }, filters);
} }
export async function getAllWebsites(userId: string) { export async function getAllWebsites(userId: string) {
@ -90,7 +90,7 @@ export async function getAllUserWebsitesIncludingTeamOwner(userId: string) {
export async function getUserWebsites( export async function getUserWebsites(
userId: string, userId: string,
filters?: PageParams, filters?: QueryFilters,
): Promise<PageResult<Website[]>> { ): Promise<PageResult<Website[]>> {
return getWebsites( return getWebsites(
{ {
@ -115,7 +115,7 @@ export async function getUserWebsites(
export async function getTeamWebsites( export async function getTeamWebsites(
teamId: string, teamId: string,
filters?: PageParams, filters?: QueryFilters,
): Promise<PageResult<Website[]>> { ): Promise<PageResult<Website[]>> {
return getWebsites( return getWebsites(
{ {

View file

@ -23,7 +23,7 @@ export async function getEventDataEvents(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { event } = filters; const { event } = filters;
const { filterParams } = await parseFilters(websiteId, filters); const { queryParams } = await parseFilters(filters);
if (event) { if (event) {
return rawQuery( return rawQuery(
@ -43,7 +43,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value group by website_event.event_name, event_data.data_key, event_data.data_type, event_data.string_value
order by 1 asc, 2 asc, 3 asc, 5 desc order by 1 asc, 2 asc, 3 asc, 5 desc
`, `,
filterParams, queryParams,
); );
} }
@ -63,7 +63,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by 1 asc, 2 asc order by 1 asc, 2 asc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }
@ -73,7 +73,7 @@ async function clickhouseQuery(
): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> { ): Promise<{ eventName: string; propertyName: string; dataType: number; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { event } = filters; const { event } = filters;
const { filterParams } = await parseFilters(websiteId, filters); const { queryParams } = await parseFilters(filters);
if (event) { if (event) {
return rawQuery( return rawQuery(
@ -92,7 +92,7 @@ async function clickhouseQuery(
order by 1 asc, 2 asc, 3 asc, 5 desc order by 1 asc, 2 asc, 3 asc, 5 desc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }
@ -110,6 +110,6 @@ async function clickhouseQuery(
order by 1 asc, 2 asc order by 1 asc, 2 asc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }

View file

@ -12,7 +12,7 @@ export async function getEventDataFields(...args: [websiteId: string, filters: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters, getDateSQL } = prisma; const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -34,7 +34,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by 2 desc order by 2 desc
limit 100 limit 100
`, `,
filterParams, queryParams,
); );
} }
@ -43,7 +43,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> { ): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -62,6 +62,6 @@ async function clickhouseQuery(
order by 2 desc order by 2 desc
limit 100 limit 100
`, `,
filterParams, queryParams,
); );
} }

View file

@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { QueryFilters, WebsiteEventData } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
export async function getEventDataProperties( export async function getEventDataProperties(
...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }] ...args: [websiteId: string, filters: QueryFilters & { propertyName?: string }]
): Promise<WebsiteEventData[]> { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -17,7 +17,7 @@ async function relationalQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
) { ) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters, { const { filterQuery, queryParams } = await parseFilters(filters, {
columns: { propertyName: 'data_key' }, columns: { propertyName: 'data_key' },
}); });
@ -36,7 +36,7 @@ async function relationalQuery(
order by 3 desc order by 3 desc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }
@ -45,7 +45,7 @@ async function clickhouseQuery(
filters: QueryFilters & { propertyName?: string }, filters: QueryFilters & { propertyName?: string },
): Promise<{ eventName: string; propertyName: string; total: number }[]> { ): Promise<{ eventName: string; propertyName: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters, { const { filterQuery, queryParams } = await parseFilters(filters, {
columns: { propertyName: 'data_key' }, columns: { propertyName: 'data_key' },
}); });
@ -63,6 +63,6 @@ async function clickhouseQuery(
order by 1, 3 desc order by 1, 3 desc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }

View file

@ -18,7 +18,7 @@ export async function getEventDataStats(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -38,7 +38,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by website_event_id, data_key group by website_event_id, data_key
) as t ) as t
`, `,
filterParams, queryParams,
); );
} }
@ -47,7 +47,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ events: number; properties: number; records: number }[]> { ): Promise<{ events: number; properties: number; records: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -67,6 +67,6 @@ async function clickhouseQuery(
group by event_id, data_key group by event_id, data_key
) as t ) as t
`, `,
filterParams, queryParams,
); );
} }

View file

@ -1,7 +1,8 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
export function getEventDataUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { export function getEventDataUsage(...args: [websiteIds: string[], filters: QueryFilters]) {
return runQuery({ return runQuery({
[PRISMA]: notImplemented, [PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -10,10 +11,10 @@ export function getEventDataUsage(...args: [websiteIds: string[], startDate: Dat
function clickhouseQuery( function clickhouseQuery(
websiteIds: string[], websiteIds: string[],
startDate: Date, filters: QueryFilters,
endDate: Date,
): Promise<{ websiteId: string; count: number }[]> { ): Promise<{ websiteId: string; count: number }[]> {
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
return rawQuery( return rawQuery(
` `

View file

@ -25,7 +25,7 @@ async function relationalQuery(
filters: QueryFilters & { eventName?: string; propertyName?: string }, filters: QueryFilters & { eventName?: string; propertyName?: string },
) { ) {
const { rawQuery, parseFilters, getDateSQL } = prisma; const { rawQuery, parseFilters, getDateSQL } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -47,7 +47,7 @@ async function relationalQuery(
order by 2 desc order by 2 desc
limit 100 limit 100
`, `,
filterParams, queryParams,
); );
} }
@ -56,7 +56,7 @@ async function clickhouseQuery(
filters: QueryFilters & { eventName?: string; propertyName?: string }, filters: QueryFilters & { eventName?: string; propertyName?: string },
): Promise<{ value: string; total: number }[]> { ): Promise<{ value: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters({ ...filters, websiteId });
return rawQuery( return rawQuery(
` `
@ -75,6 +75,6 @@ async function clickhouseQuery(
order by 2 desc order by 2 desc
limit 100 limit 100
`, `,
filterParams, queryParams,
); );
} }

View file

@ -22,7 +22,7 @@ export async function getEventMetrics(
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = prisma; const { rawQuery, getDateSQL, parseFilters } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(websiteId, { const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
...filters, ...filters,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
}); });
@ -34,7 +34,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} t, ${getDateSQL('website_event.created_at', unit, timezone)} t,
count(*) y count(*) y
from website_event from website_event
${joinSession} ${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}} and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}} and event_type = {{eventType}}
@ -42,7 +42,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by 1, 2 group by 1, 2
order by 2 order by 2
`, `,
filterParams, queryParams,
); );
} }
@ -52,7 +52,7 @@ async function clickhouseQuery(
): Promise<{ x: string; t: string; y: number }[]> { ): Promise<{ x: string; t: string; y: number }[]> {
const { timezone = 'UTC', unit = 'day' } = filters; const { timezone = 'UTC', unit = 'day' } = filters;
const { rawQuery, getDateSQL, parseFilters } = clickhouse; const { rawQuery, getDateSQL, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, { const { filterQuery, queryParams } = await parseFilters({
...filters, ...filters,
eventType: EVENT_TYPE.customEvent, eventType: EVENT_TYPE.customEvent,
}); });
@ -92,5 +92,5 @@ async function clickhouseQuery(
`; `;
} }
return rawQuery(sql, filterParams); return rawQuery(sql, queryParams);
} }

View file

@ -1,7 +1,8 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery, notImplemented } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
export function getEventUsage(...args: [websiteIds: string[], startDate: Date, endDate: Date]) { export function getEventUsage(...args: [websiteIds: string[], filters: QueryFilters]) {
return runQuery({ return runQuery({
[PRISMA]: notImplemented, [PRISMA]: notImplemented,
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -10,10 +11,10 @@ export function getEventUsage(...args: [websiteIds: string[], startDate: Date, e
function clickhouseQuery( function clickhouseQuery(
websiteIds: string[], websiteIds: string[],
startDate: Date, filters: QueryFilters,
endDate: Date,
): Promise<{ websiteId: string; count: number }[]> { ): Promise<{ websiteId: string; count: number }[]> {
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
return rawQuery( return rawQuery(
` `

View file

@ -1,26 +1,27 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { PageParams, QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
export function getWebsiteEvents( export function getWebsiteEvents(...args: [websiteId: string, filters: QueryFilters]) {
...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams]
) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
}); });
} }
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = prisma; const { pagedRawQuery, parseFilters } = prisma;
const { search } = pageParams; const { search } = filters;
const { filterQuery, filterParams } = await parseFilters(websiteId, { const { filterQuery, queryParams } = await parseFilters({
...filters, ...filters,
websiteId,
}); });
const db = getDatabaseType(); const searchQuery = filters.search
const like = db === POSTGRESQL ? 'ilike' : 'like'; ? `and ((event_name ilike {{search}} and event_type = 2)
or (url_path ilike {{search}} and event_type = 1))`
: '';
return pagedRawQuery( return pagedRawQuery(
` `
@ -41,25 +42,27 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}} and created_at between {{startDate}} and {{endDate}}
${filterQuery} ${filterQuery}
${ ${searchQuery}
search
? `and ((event_name ${like} {{search}} and event_type = 2)
or (url_path ${like} {{search}} and event_type = 1))`
: ''
}
order by created_at desc order by created_at desc
`, `,
{ ...filterParams, search: `%${search}%` }, { ...queryParams, search: `%${search}%` },
pageParams, filters,
); );
} }
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) { async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
const { pagedQuery, parseFilters } = clickhouse; const { pagedRawQuery, parseFilters } = clickhouse;
const { filterParams, dateQuery, filterQuery } = await parseFilters(websiteId, filters); const { queryParams, dateQuery, filterQuery } = await parseFilters({
const { search } = pageParams; ...filters,
websiteId,
});
return pagedQuery( const searchQuery = filters.search
? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = 2)
or (positionCaseInsensitive(url_path, {search:String}) > 0 and event_type = 1))`
: '';
return pagedRawQuery(
` `
select select
event_id as id, event_id as id,
@ -78,15 +81,10 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
${dateQuery} ${dateQuery}
${filterQuery} ${filterQuery}
${ ${searchQuery}
search
? `and ((positionCaseInsensitive(event_name, {search:String}) > 0 and event_type = 2)
or (positionCaseInsensitive(url_path, {search:String}) > 0 and event_type = 1))`
: ''
}
order by created_at desc order by created_at desc
`, `,
{ ...filterParams, search }, queryParams,
pageParams, filters,
); );
} }

View file

@ -12,7 +12,7 @@ export async function getChannelMetrics(...args: [websiteId: string, filters?: Q
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters); const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -27,7 +27,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by 1, 2 group by 1, 2
order by visitors desc order by visitors desc
`, `,
filterParams, queryParams,
); );
} }
@ -36,7 +36,7 @@ async function clickhouseQuery(
filters: QueryFilters, filters: QueryFilters,
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters); const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
const sql = ` const sql = `
select select
@ -51,5 +51,5 @@ async function clickhouseQuery(
order by visitors desc order by visitors desc
`; `;
return rawQuery(sql, filterParams); return rawQuery(sql, queryParams);
} }

View file

@ -12,7 +12,7 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters:
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters); const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -35,13 +35,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
order by website_event.created_at desc order by website_event.created_at desc
limit 100 limit 100
`, `,
filterParams, queryParams,
); );
} }
async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> { async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promise<{ x: number }> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterParams, filterQuery, dateQuery } = await parseFilters(websiteId, filters); const { queryParams, filterQuery, dateQuery } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -62,6 +62,6 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
order by createdAt desc order by createdAt desc
limit 100 limit 100
`, `,
{ ...filters, ...filterParams }, { ...filters, ...queryParams },
); );
} }

View file

@ -1,4 +1,5 @@
import { getPageviewStats, getRealtimeActivity, getSessionStats } from '@/queries'; import { getPageviewStats, getRealtimeActivity, getSessionStats } from '@/queries';
import { QueryFilters } from '@/lib/types';
function increment(data: object, key: string) { function increment(data: object, key: string) {
if (key) { if (key) {
@ -10,12 +11,7 @@ function increment(data: object, key: string) {
} }
} }
export async function getRealtimeData( export async function getRealtimeData(websiteId: string, filters: QueryFilters) {
websiteId: string,
criteria: { startDate: Date; timezone: string },
) {
const { startDate, timezone } = criteria;
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
const [activity, pageviews, sessions] = await Promise.all([ const [activity, pageviews, sessions] = await Promise.all([
getRealtimeActivity(websiteId, filters), getRealtimeActivity(websiteId, filters),
getPageviewStats(websiteId, filters), getPageviewStats(websiteId, filters),

View file

@ -1,9 +1,10 @@
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db'; import { runQuery, CLICKHOUSE, PRISMA } from '@/lib/db';
import { QueryFilters } from '@/lib/types';
export async function getValues( export async function getValues(
...args: [websiteId: string, column: string, startDate: Date, endDate: Date, search: string] ...args: [websiteId: string, column: string, filters: QueryFilters]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -11,15 +12,11 @@ export async function getValues(
}); });
} }
async function relationalQuery( async function relationalQuery(websiteId: string, column: string, filters: QueryFilters) {
websiteId: string,
column: string,
startDate: Date,
endDate: Date,
search: string,
) {
const { rawQuery, getSearchSQL } = prisma; const { rawQuery, getSearchSQL } = prisma;
const params = {}; const params = {};
const { startDate, endDate, search } = filters;
let searchQuery = ''; let searchQuery = '';
if (search) { if (search) {
@ -63,15 +60,11 @@ async function relationalQuery(
); );
} }
async function clickhouseQuery( async function clickhouseQuery(websiteId: string, column: string, filters: QueryFilters) {
websiteId: string,
column: string,
startDate: Date,
endDate: Date,
search: string,
) {
const { rawQuery, getSearchSQL } = clickhouse; const { rawQuery, getSearchSQL } = clickhouse;
const params = {}; const params = {};
const { startDate, endDate, search } = filters;
let searchQuery = ''; let searchQuery = '';
if (search) { if (search) {

View file

@ -12,8 +12,9 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) {
async function relationalQuery(websiteId: string) { async function relationalQuery(websiteId: string) {
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterParams } = await parseFilters(websiteId, { const { queryParams } = await parseFilters({
startDate: new Date(DEFAULT_RESET_DATE), startDate: new Date(DEFAULT_RESET_DATE),
websiteId,
}); });
const result = await rawQuery( const result = await rawQuery(
@ -25,7 +26,7 @@ async function relationalQuery(websiteId: string) {
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}} and created_at >= {{startDate}}
`, `,
filterParams, queryParams,
); );
return result[0] ?? null; return result[0] ?? null;
@ -33,8 +34,9 @@ async function relationalQuery(websiteId: string) {
async function clickhouseQuery(websiteId: string) { async function clickhouseQuery(websiteId: string) {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterParams } = await parseFilters(websiteId, { const { queryParams } = await parseFilters({
startDate: new Date(DEFAULT_RESET_DATE), startDate: new Date(DEFAULT_RESET_DATE),
websiteId,
}); });
const result = await rawQuery( const result = await rawQuery(
@ -46,7 +48,7 @@ async function clickhouseQuery(websiteId: string) {
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64} and created_at >= {startDate:DateTime64}
`, `,
filterParams, queryParams,
); );
return result[0] ?? null; return result[0] ?? null;

View file

@ -5,11 +5,17 @@ import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types'; import { QueryFilters } from '@/lib/types';
import { EVENT_COLUMNS } from '@/lib/constants'; import { EVENT_COLUMNS } from '@/lib/constants';
export interface WebsiteStatsData {
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
}
export async function getWebsiteStats( export async function getWebsiteStats(
...args: [websiteId: string, filters: QueryFilters] ...args: [websiteId: string, filters: QueryFilters]
): Promise< ): Promise<WebsiteStatsData[]> {
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args),
@ -19,12 +25,11 @@ export async function getWebsiteStats(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<WebsiteStatsData[]> {
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(websiteId, { const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
@ -44,7 +49,7 @@ async function relationalQuery(
min(website_event.created_at) as "min_time", min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time" max(website_event.created_at) as "max_time"
from website_event from website_event
${joinSession} ${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}} and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}} and event_type = {{eventType}}
@ -52,19 +57,18 @@ async function relationalQuery(
group by 1, 2 group by 1, 2
) as t ) as t
`, `,
filterParams, queryParams,
); );
} }
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
filters: QueryFilters, filters: QueryFilters,
): Promise< ): Promise<WebsiteStatsData[]> {
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
> {
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, { const { filterQuery, queryParams } = await parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
@ -117,5 +121,5 @@ async function clickhouseQuery(
`; `;
} }
return rawQuery(sql, filterParams).then(result => result?.[0]); return rawQuery(sql, queryParams).then(result => result?.[0]);
} }

View file

@ -28,8 +28,7 @@ async function relationalQuery(
) { ) {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters( const { filterQuery, joinSessionQuery, queryParams } = await parseFilters(
websiteId,
{ {
...filters, ...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
@ -70,7 +69,7 @@ async function relationalQuery(
select ${column} x, select ${column} x,
${column === 'referrer_domain' ? 'count(distinct website_event.session_id)' : 'count(*)'} as y ${column === 'referrer_domain' ? 'count(distinct website_event.session_id)' : 'count(*)'} as y
from website_event from website_event
${joinSession} ${joinSessionQuery}
${entryExitQuery} ${entryExitQuery}
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}} and website_event.created_at between {{startDate}} and {{endDate}}
@ -82,7 +81,7 @@ async function relationalQuery(
limit ${limit} limit ${limit}
offset ${offset} offset ${offset}
`, `,
filterParams, queryParams,
); );
} }
@ -95,7 +94,7 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type; const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, { const { filterQuery, queryParams } = await parseFilters({
...filters, ...filters,
eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventType: column === 'event_name' ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
}); });
@ -180,5 +179,5 @@ async function clickhouseQuery(
`; `;
} }
return rawQuery(sql, filterParams); return rawQuery(sql, queryParams);
} }

View file

@ -14,8 +14,9 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que
async function relationalQuery(websiteId: string, filters: QueryFilters) { async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { getDateSQL, parseFilters, rawQuery } = prisma; const { getDateSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters(websiteId, { const { filterQuery, joinSessionQuery, queryParams } = await parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
@ -25,7 +26,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
${getDateSQL('website_event.created_at', unit, timezone)} x, ${getDateSQL('website_event.created_at', unit, timezone)} x,
count(*) y count(*) y
from website_event from website_event
${joinSession} ${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}} and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}} and event_type = {{eventType}}
@ -33,7 +34,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
group by 1 group by 1
order by 1 order by 1
`, `,
filterParams, queryParams,
); );
} }
@ -43,8 +44,9 @@ async function clickhouseQuery(
): Promise<{ x: string; y: number }[]> { ): Promise<{ x: string; y: number }[]> {
const { timezone = 'utc', unit = 'day' } = filters; const { timezone = 'utc', unit = 'day' } = filters;
const { parseFilters, rawQuery, getDateSQL } = clickhouse; const { parseFilters, rawQuery, getDateSQL } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, { const { filterQuery, queryParams } = await parseFilters({
...filters, ...filters,
websiteId,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
@ -88,5 +90,5 @@ async function clickhouseQuery(
`; `;
} }
return rawQuery(sql, filterParams); return rawQuery(sql, queryParams);
} }

View file

@ -244,7 +244,7 @@ async function clickhouseQuery(
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent; const eventType = type === 'page' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
const column = type === 'page' ? 'url_path' : 'event_name'; const column = type === 'page' ? 'url_path' : 'event_name';
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, queryParams } = await parseFilters(criteria);
function getUTMQuery(utmColumn: string) { function getUTMQuery(utmColumn: string) {
return ` return `
@ -345,7 +345,7 @@ async function clickhouseQuery(
order by 2 desc order by 2 desc
limit 20 limit 20
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const paidAdsres = await rawQuery< const paidAdsres = await rawQuery<
@ -376,7 +376,7 @@ async function clickhouseQuery(
order by 2 desc order by 2 desc
limit 20 limit 20
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const sourceRes = await rawQuery< const sourceRes = await rawQuery<
@ -390,7 +390,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_source')} ${getUTMQuery('utm_source')}
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const mediumRes = await rawQuery< const mediumRes = await rawQuery<
@ -404,7 +404,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_medium')} ${getUTMQuery('utm_medium')}
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const campaignRes = await rawQuery< const campaignRes = await rawQuery<
@ -418,7 +418,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_campaign')} ${getUTMQuery('utm_campaign')}
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const contentRes = await rawQuery< const contentRes = await rawQuery<
@ -432,7 +432,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_content')} ${getUTMQuery('utm_content')}
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const termRes = await rawQuery< const termRes = await rawQuery<
@ -446,7 +446,7 @@ async function clickhouseQuery(
${getModelQuery(model)} ${getModelQuery(model)}
${getUTMQuery('utm_term')} ${getUTMQuery('utm_term')}
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
); );
const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>( const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>(
@ -462,7 +462,7 @@ async function clickhouseQuery(
and event_type = {eventType:UInt32} and event_type = {eventType:UInt32}
${filterQuery} ${filterQuery}
`, `,
{ ...filterParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency }, { ...queryParams, websiteId, startDate, endDate, conversionStep: step, eventType, currency },
).then(result => result?.[0]); ).then(result => result?.[0]);
return { return {

View file

@ -24,8 +24,7 @@ async function relationalQuery(
}[] }[]
> { > {
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma; const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
const { filterQuery, joinSession, filterParams } = await parseFilters( const { filterQuery, joinSessionQuery, queryParams } = await parseFilters(
websiteId,
{ {
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
@ -53,7 +52,7 @@ async function relationalQuery(
min(website_event.created_at) as "min_time", min(website_event.created_at) as "min_time",
max(website_event.created_at) as "max_time" max(website_event.created_at) as "max_time"
from website_event from website_event
${joinSession} ${joinSessionQuery}
where website_event.website_id = {{websiteId::uuid}} where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}} and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}} and event_type = {{eventType}}
@ -65,7 +64,7 @@ async function relationalQuery(
order by 1 desc, 2 desc order by 1 desc, 2 desc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }
@ -80,7 +79,7 @@ async function clickhouseQuery(
}[] }[]
> { > {
const { parseFilters, rawQuery } = clickhouse; const { parseFilters, rawQuery } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, { const { filterQuery, queryParams } = await parseFilters({
...filters, ...filters,
eventType: EVENT_TYPE.pageView, eventType: EVENT_TYPE.pageView,
}); });
@ -114,7 +113,7 @@ async function clickhouseQuery(
order by 1 desc, 2 desc order by 1 desc, 2 desc
limit 500 limit 500
`, `,
filterParams, queryParams,
); );
} }

View file

@ -126,7 +126,7 @@ async function clickhouseQuery(
steps, steps,
windowMinutes, windowMinutes,
); );
const { filterQuery, filterParams: filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, queryParams } = await parseFilters(criteria);
function getFunnelQuery( function getFunnelQuery(
steps: { type: string; value: string }[], steps: { type: string; value: string }[],
@ -136,7 +136,7 @@ async function clickhouseQuery(
levelQuery: string; levelQuery: string;
sumQuery: string; sumQuery: string;
stepFilterQuery: string; stepFilterQuery: string;
params: { [key: string]: string }; params: Record<string, string>;
} { } {
return steps.reduce( return steps.reduce(
(pv, cv, i) => { (pv, cv, i) => {
@ -215,7 +215,7 @@ async function clickhouseQuery(
startDate, startDate,
endDate, endDate,
...params, ...params,
...filterParams, ...queryParams,
}, },
).then(formatResults(steps)); ).then(formatResults(steps));
} }

View file

@ -1,6 +1,7 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export interface GoalCriteria { export interface GoalCriteria {
startDate: Date; startDate: Date;
@ -9,6 +10,7 @@ export interface GoalCriteria {
value: string; value: string;
operator?: string; operator?: string;
property?: string; property?: string;
filters: QueryFilters;
} }
export async function getGoal(...args: [websiteId: string, criteria: GoalCriteria]) { export async function getGoal(...args: [websiteId: string, criteria: GoalCriteria]) {
@ -19,9 +21,13 @@ export async function getGoal(...args: [websiteId: string, criteria: GoalCriteri
} }
async function relationalQuery(websiteId: string, criteria: GoalCriteria) { async function relationalQuery(websiteId: string, criteria: GoalCriteria) {
const { type, value } = criteria; const { type, value, filters } = criteria;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, dateQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, dateQuery, queryParams } = await parseFilters({
...filters,
websiteId,
value,
});
const isPage = type === 'page'; const isPage = type === 'page';
const column = isPage ? 'url_path' : 'event_name'; const column = isPage ? 'url_path' : 'event_name';
const eventType = isPage ? 1 : 2; const eventType = isPage ? 1 : 2;
@ -43,14 +49,18 @@ async function relationalQuery(websiteId: string, criteria: GoalCriteria) {
${dateQuery} ${dateQuery}
${filterQuery} ${filterQuery}
`, `,
{ ...filterParams, value }, queryParams,
); );
} }
async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) { async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) {
const { type, value } = criteria; const { type, value, filters } = criteria;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, dateQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, dateQuery, queryParams } = await parseFilters({
...filters,
websiteId,
value,
});
const isPage = type === 'page'; const isPage = type === 'page';
const column = isPage ? 'url_path' : 'event_name'; const column = isPage ? 'url_path' : 'event_name';
const eventType = isPage ? 1 : 2; const eventType = isPage ? 1 : 2;
@ -71,6 +81,6 @@ async function clickhouseQuery(websiteId: string, criteria: GoalCriteria) {
${dateQuery} ${dateQuery}
${filterQuery} ${filterQuery}
`, `,
{ ...filterParams, value }, queryParams,
).then(results => results?.[0]); ).then(results => results?.[0]);
} }

View file

@ -57,7 +57,7 @@ async function relationalQuery(
sequenceQuery: string; sequenceQuery: string;
startStepQuery: string; startStepQuery: string;
endStepQuery: string; endStepQuery: string;
params: { [key: string]: string }; params: Record<string, string>;
} { } {
const params = {}; const params = {};
let sequenceQuery = ''; let sequenceQuery = '';
@ -108,7 +108,7 @@ async function relationalQuery(
sequenceQuery, sequenceQuery,
startStepQuery, startStepQuery,
endStepQuery, endStepQuery,
filterParams: params, params,
}; };
} }
@ -167,7 +167,7 @@ async function clickhouseQuery(
sequenceQuery: string; sequenceQuery: string;
startStepQuery: string; startStepQuery: string;
endStepQuery: string; endStepQuery: string;
params: { [key: string]: string }; params: Record<string, string>;
} { } {
const params = {}; const params = {};
let sequenceQuery = ''; let sequenceQuery = '';
@ -218,11 +218,11 @@ async function clickhouseQuery(
sequenceQuery, sequenceQuery,
startStepQuery, startStepQuery,
endStepQuery, endStepQuery,
filterParams: params, params,
}; };
} }
const { filterQuery, filterParams: filterParams } = await parseFilters(websiteId, filters); const { filterQuery, queryParams } = await parseFilters(filters);
return rawQuery( return rawQuery(
` `
@ -249,7 +249,7 @@ async function clickhouseQuery(
startDate, startDate,
endDate, endDate,
...params, ...params,
...filterParams, ...queryParams,
}, },
).then(parseResult); ).then(parseResult);
} }

View file

@ -31,7 +31,7 @@ async function relationalQuery(
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma; const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery, parseFilters } = prisma;
const unit = 'day'; const unit = 'day';
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery( return rawQuery(
` `
@ -85,7 +85,7 @@ async function relationalQuery(
websiteId, websiteId,
startDate, startDate,
endDate, endDate,
...filterParams, ...queryParams,
}, },
); );
} }
@ -98,7 +98,7 @@ async function clickhouseQuery(
const { getDateSQL, rawQuery, parseFilters } = clickhouse; const { getDateSQL, rawQuery, parseFilters } = clickhouse;
const unit = 'day'; const unit = 'day';
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery( return rawQuery(
` `
@ -154,7 +154,7 @@ async function clickhouseQuery(
websiteId, websiteId,
startDate, startDate,
endDate, endDate,
...filterParams, ...queryParams,
}, },
); );
} }

View file

@ -17,7 +17,7 @@ export async function getUTM(...args: [websiteId: string, criteria: UTMCriteria]
async function relationalQuery(websiteId: string, criteria: UTMCriteria) { async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
const { startDate, endDate } = criteria; const { startDate, endDate } = criteria;
const { rawQuery, parseFilters } = prisma; const { rawQuery, parseFilters } = prisma;
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery( return rawQuery(
` `
@ -31,7 +31,7 @@ async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
group by 1 group by 1
`, `,
{ {
...filterParams, ...queryParams,
websiteId, websiteId,
startDate, startDate,
endDate, endDate,
@ -42,7 +42,7 @@ async function relationalQuery(websiteId: string, criteria: UTMCriteria) {
async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) { async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) {
const { startDate, endDate } = criteria; const { startDate, endDate } = criteria;
const { rawQuery, parseFilters } = clickhouse; const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, filterParams } = await parseFilters(websiteId, criteria); const { filterQuery, queryParams } = await parseFilters(criteria);
return rawQuery( return rawQuery(
` `
@ -56,7 +56,7 @@ async function clickhouseQuery(websiteId: string, criteria: UTMCriteria) {
group by 1 group by 1
`, `,
{ {
...filterParams, ...queryParams,
websiteId, websiteId,
startDate, startDate,
endDate, endDate,

View file

@ -1,9 +1,10 @@
import clickhouse from '@/lib/clickhouse'; import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { QueryFilters } from '@/lib/types';
export async function getSessionActivity( export async function getSessionActivity(
...args: [websiteId: string, sessionId: string, startDate: Date, endDate: Date] ...args: [websiteId: string, sessionId: string, filters: QueryFilters]
) { ) {
return runQuery({ return runQuery({
[PRISMA]: () => relationalQuery(...args), [PRISMA]: () => relationalQuery(...args),
@ -11,12 +12,9 @@ export async function getSessionActivity(
}); });
} }
async function relationalQuery( async function relationalQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
websiteId: string, const { startDate, endDate } = filters;
sessionId: string,
startDate: Date,
endDate: Date,
) {
return prisma.client.websiteEvent.findMany({ return prisma.client.websiteEvent.findMany({
where: { where: {
sessionId, sessionId,
@ -28,13 +26,9 @@ async function relationalQuery(
}); });
} }
async function clickhouseQuery( async function clickhouseQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
websiteId: string,
sessionId: string,
startDate: Date,
endDate: Date,
) {
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
return rawQuery( return rawQuery(
` `

Some files were not shown because too many files have changed in this diff Show more