mirror of
https://github.com/umami-software/umami.git
synced 2026-02-07 22:27:16 +01:00
Refactor part 2: Electric Boogaloo. Standardize way of passing filter parameters.
This commit is contained in:
parent
f26f1b0581
commit
cdf391d5c2
90 changed files with 867 additions and 709 deletions
|
|
@ -136,7 +136,7 @@ function getQueryParams(filters: Record<string, any>) {
|
|||
};
|
||||
}
|
||||
|
||||
async function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
|
||||
function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
|
||||
return {
|
||||
filterQuery: getFilterQuery(filters, options),
|
||||
dateQuery: getDateQuery(filters),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
|
|||
function getClient() {
|
||||
const { username, password } = new URL(process.env.KAFKA_URL);
|
||||
const brokers = process.env.KAFKA_BROKER.split(',');
|
||||
const mechanism = process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512';
|
||||
const mechanism =
|
||||
(process.env.KAFKA_SASL_MECHANISM as 'plain' | 'scram-sha-256' | 'scram-sha-512') || 'plain';
|
||||
|
||||
const ssl: { ssl?: tls.ConnectionOptions | boolean; sasl?: SASLOptions } =
|
||||
username && password
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export function filtersToArray(filters: QueryFilters, options: QueryOptions = {}
|
|||
column: options?.columns?.[key] ?? FILTER_COLUMNS[key],
|
||||
operator,
|
||||
value,
|
||||
prefix: options?.prefix,
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,15 +84,10 @@ function getTimestampDiffSQL(field1: string, field2: string): string {
|
|||
}
|
||||
|
||||
function getSearchSQL(column: string, param: string = 'search'): string {
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
|
||||
return `and ${column} ${like} {{${param}}}`;
|
||||
return `and ${column} ilike {{${param}}}`;
|
||||
}
|
||||
|
||||
function mapFilter(column: string, operator: string, name: string, type: string = '') {
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
const value = `{{${name}${type ? `::${type}` : ''}}}`;
|
||||
|
||||
switch (operator) {
|
||||
|
|
@ -101,28 +96,31 @@ function mapFilter(column: string, operator: string, name: string, type: string
|
|||
case OPERATORS.notEquals:
|
||||
return `${column} != ${value}`;
|
||||
case OPERATORS.contains:
|
||||
return `${column} ${like} ${value}`;
|
||||
return `${column} ilike ${value}`;
|
||||
case OPERATORS.doesNotContain:
|
||||
return `${column} not ${like} ${value}`;
|
||||
return `${column} not ilike ${value}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
|
||||
const query = filtersToArray(filters, options).reduce((arr, { name, column, operator }) => {
|
||||
if (column) {
|
||||
arr.push(`and ${mapFilter(column, operator, name)}`);
|
||||
const query = filtersToArray(filters, options).reduce(
|
||||
(arr, { name, column, operator, prefix = '' }) => {
|
||||
if (column) {
|
||||
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name)}`);
|
||||
|
||||
if (name === 'referrer') {
|
||||
arr.push(
|
||||
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
|
||||
);
|
||||
if (name === 'referrer') {
|
||||
arr.push(
|
||||
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, []);
|
||||
return arr;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return query.join('\n');
|
||||
}
|
||||
|
|
@ -154,7 +152,7 @@ function getQueryParams(filters: Record<string, any>) {
|
|||
};
|
||||
}
|
||||
|
||||
async function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
|
||||
function parseFilters(filters: Record<string, any>, options?: QueryOptions) {
|
||||
const joinSession = Object.keys(filters).find(key =>
|
||||
['referrer', ...SESSION_COLUMNS].includes(key),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from 'zod/v4';
|
||||
import { FILTER_COLUMNS, DEFAULT_PAGE_SIZE } from '@/lib/constants';
|
||||
import { badRequest, unauthorized } from '@/lib/response';
|
||||
import { getAllowedUnits, getCompareDate, getMinimumUnit, maxDate } from '@/lib/date';
|
||||
import { getAllowedUnits, getMinimumUnit, maxDate } from '@/lib/date';
|
||||
import { checkAuth } from '@/lib/auth';
|
||||
import { fetchWebsite } from '@/lib/load';
|
||||
import { QueryFilters } from '@/lib/types';
|
||||
|
|
@ -50,23 +50,14 @@ export async function getJsonBody(request: Request) {
|
|||
}
|
||||
|
||||
export function getRequestDateRange(query: Record<string, string>) {
|
||||
const { startAt, endAt, unit, compare, timezone } = query;
|
||||
const { startAt, endAt, unit, timezone } = query;
|
||||
|
||||
const startDate = new Date(+startAt);
|
||||
const endDate = new Date(+endAt);
|
||||
|
||||
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
|
||||
compare,
|
||||
startDate,
|
||||
endDate,
|
||||
);
|
||||
|
||||
return {
|
||||
startDate,
|
||||
endDate,
|
||||
compare,
|
||||
compareStartDate,
|
||||
compareEndDate,
|
||||
timezone,
|
||||
unit: getAllowedUnits(startDate, endDate).includes(unit)
|
||||
? unit
|
||||
|
|
@ -86,11 +77,21 @@ export function getRequestFilters(query: Record<string, any>) {
|
|||
}, {});
|
||||
}
|
||||
|
||||
export async function getQueryFilters(params: Record<string, any>): Promise<QueryFilters> {
|
||||
export async function setWebsiteDate(websiteId: string, data: Record<string, any>) {
|
||||
const website = await fetchWebsite(websiteId);
|
||||
|
||||
if (website) {
|
||||
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getQueryFilters(params: Record<string, any>): QueryFilters {
|
||||
const dateRange = getRequestDateRange(params);
|
||||
const filters = getRequestFilters(params);
|
||||
|
||||
const data = {
|
||||
return {
|
||||
...dateRange,
|
||||
...filters,
|
||||
page: params?.page,
|
||||
|
|
@ -98,17 +99,5 @@ export async function getQueryFilters(params: Record<string, any>): Promise<Quer
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value),
|
|||
});
|
||||
|
||||
export const dateRangeParams = {
|
||||
startAt: z.coerce.number(),
|
||||
endAt: z.coerce.number(),
|
||||
startAt: z.coerce.number().optional(),
|
||||
endAt: z.coerce.number().optional(),
|
||||
startDate: z.coerce.date().optional(),
|
||||
endDate: z.coerce.date().optional(),
|
||||
timezone: timezoneParam.optional(),
|
||||
unit: unitParam.optional(),
|
||||
compare: z.string().optional(),
|
||||
|
|
@ -33,12 +35,12 @@ export const filterParams = {
|
|||
hostname: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
event: z.string().optional(),
|
||||
search: z.string().optional(),
|
||||
};
|
||||
|
||||
export const pagingParams = {
|
||||
page: z.coerce.number().int().positive().optional(),
|
||||
pageSize: z.coerce.number().int().positive().optional(),
|
||||
search: z.string().optional(),
|
||||
};
|
||||
|
||||
export const sortingParams = {
|
||||
|
|
@ -93,23 +95,63 @@ export const reportTypeParam = z.enum([
|
|||
'utm',
|
||||
]);
|
||||
|
||||
export const reportParms = {
|
||||
websiteId: z.string().uuid(),
|
||||
dateRange: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
timezone: timezoneParam.optional(),
|
||||
unit: unitParam.optional(),
|
||||
compare: z.string().optional(),
|
||||
compareStartDate: z.coerce.date().optional(),
|
||||
compareEndDate: z.coerce.date().optional(),
|
||||
}),
|
||||
};
|
||||
export const dateRangeSchema = z.object({ ...dateRangeParams }).superRefine((data, ctx) => {
|
||||
const hasTimestamps = data.startAt !== undefined && data.endAt !== undefined;
|
||||
const hasDates = data.startDate !== undefined && data.endDate !== undefined;
|
||||
|
||||
if (!hasTimestamps && !hasDates) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'You must provide either startAt & endAt or startDate & endDate.',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasTimestamps && hasDates) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Provide either startAt & endAt or startDate & endDate, not both.',
|
||||
});
|
||||
}
|
||||
|
||||
if (data.startAt !== undefined && data.endAt === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'If you provide startAt, you must also provide endAt.',
|
||||
path: ['endAt'],
|
||||
});
|
||||
}
|
||||
|
||||
if (data.endAt !== undefined && data.startAt === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'If you provide endAt, you must also provide startAt.',
|
||||
path: ['startAt'],
|
||||
});
|
||||
}
|
||||
|
||||
if (data.startDate !== undefined && data.endDate === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'If you provide startDate, you must also provide endDate.',
|
||||
path: ['endDate'],
|
||||
});
|
||||
}
|
||||
|
||||
if (data.endDate !== undefined && data.startDate === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'If you provide endDate, you must also provide startDate.',
|
||||
path: ['startDate'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const goalReportSchema = z.object({
|
||||
type: z.literal('goal'),
|
||||
parameters: z
|
||||
.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
type: z.string(),
|
||||
value: z.string(),
|
||||
operator: z.enum(['count', 'sum', 'average']).optional(),
|
||||
|
|
@ -126,6 +168,8 @@ export const goalReportSchema = z.object({
|
|||
export const funnelReportSchema = z.object({
|
||||
type: z.literal('funnel'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
window: z.coerce.number().positive(),
|
||||
steps: z
|
||||
.array(
|
||||
|
|
@ -142,6 +186,8 @@ export const funnelReportSchema = z.object({
|
|||
export const journeyReportSchema = z.object({
|
||||
type: z.literal('journey'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
steps: z.coerce.number().min(2).max(7),
|
||||
startStep: z.string().optional(),
|
||||
endStep: z.string().optional(),
|
||||
|
|
@ -150,15 +196,27 @@ export const journeyReportSchema = z.object({
|
|||
|
||||
export const retentionReportSchema = z.object({
|
||||
type: z.literal('retention'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
timezone: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const utmReportSchema = z.object({
|
||||
type: z.literal('utm'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const revenueReportSchema = z.object({
|
||||
type: z.literal('revenue'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
timezone: z.string().optional(),
|
||||
currency: z.string(),
|
||||
}),
|
||||
});
|
||||
|
|
@ -166,6 +224,8 @@ export const revenueReportSchema = z.object({
|
|||
export const attributionReportSchema = z.object({
|
||||
type: z.literal('attribution'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
model: z.enum(['first-click', 'last-click']),
|
||||
type: z.enum(['page', 'event']),
|
||||
step: z.string(),
|
||||
|
|
@ -176,6 +236,8 @@ export const attributionReportSchema = z.object({
|
|||
export const breakdownReportSchema = z.object({
|
||||
type: z.literal('breakdown'),
|
||||
parameters: z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
fields: z.array(fieldsParam),
|
||||
}),
|
||||
});
|
||||
|
|
@ -202,7 +264,7 @@ export const reportSchema = z.intersection(reportBaseSchema, reportTypeSchema);
|
|||
|
||||
export const reportResultSchema = z.intersection(
|
||||
z.object({
|
||||
...reportParms,
|
||||
websiteId: z.string().uuid(),
|
||||
filters: z.object({ ...filterParams }),
|
||||
}),
|
||||
reportTypeSchema,
|
||||
|
|
|
|||
|
|
@ -41,19 +41,20 @@ export interface QueryOptions {
|
|||
joinSession?: boolean;
|
||||
columns?: Record<string, string>;
|
||||
limit?: number;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export interface QueryFilters {
|
||||
websiteId?: string;
|
||||
// Date range
|
||||
export interface QueryFilters extends DateParams, FilterParams, SortParams, PageParams {}
|
||||
|
||||
export interface DateParams {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
compareStartDate?: Date;
|
||||
compareEndDate?: Date;
|
||||
compare?: string;
|
||||
unit?: string;
|
||||
timezone?: string;
|
||||
// Filters
|
||||
compareDate?: Date;
|
||||
}
|
||||
|
||||
export interface FilterParams {
|
||||
path?: string;
|
||||
referrer?: string;
|
||||
title?: string;
|
||||
|
|
@ -70,20 +71,16 @@ export interface QueryFilters {
|
|||
search?: string;
|
||||
tag?: string;
|
||||
eventType?: number;
|
||||
// Paging
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
// Sorting
|
||||
}
|
||||
|
||||
export interface SortParams {
|
||||
orderBy?: string;
|
||||
sortDescending?: boolean;
|
||||
}
|
||||
|
||||
export interface PageParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
orderBy?: string;
|
||||
sortDescending?: boolean;
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue