Skip to content

Commit

Permalink
Added Count Result Aggregation models and helpers from foundatio. Thi…
Browse files Browse the repository at this point in the history
…s allows us fully typed aggregations via the api.
  • Loading branch information
niemyjski committed Dec 25, 2024
1 parent ebb8227 commit 9fd7d34
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { WebSocketMessageValue } from '$features/websockets/models';
import type { CountResult } from '$shared/models';

import { accessToken } from '$features/auth/index.svelte';
import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
Expand All @@ -17,21 +18,35 @@ export async function invalidatePersistentEventQueries(queryClient: QueryClient,
}

if (project_id) {
await queryClient.invalidateQueries({ queryKey: queryKeys.projects(project_id) });
await queryClient.invalidateQueries({ queryKey: queryKeys.projectsCount(project_id) });
}

if (!id && !stack_id) {
await queryClient.invalidateQueries({ queryKey: queryKeys.type });
} else {
await queryClient.invalidateQueries({ queryKey: queryKeys.count() });
}
}

export const queryKeys = {
count: () => [...queryKeys.type, 'count'] as const,
id: (id: string | undefined) => [...queryKeys.type, id] as const,
projects: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
projectsCount: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
stacks: (id: string | undefined) => [...queryKeys.type, 'stacks', id] as const,
stacksCount: (id: string | undefined) => [...queryKeys.stacks(id), 'count'] as const,
type: ['PersistentEvent'] as const
};

export interface GetCountRequest {
params?: {
aggregations?: string;
filter?: string;
mode?: 'stack_new';
offset?: string;
time?: string;
};
}

export interface GetEventRequest {
route: {
id: string | undefined;
Expand Down Expand Up @@ -93,6 +108,25 @@ export interface GetStackEventsRequest {
};
}

export function getCountQuery(request: GetCountRequest) {
const queryClient = useQueryClient();

return createQuery<CountResult, ProblemDetails>(() => ({
enabled: () => !!accessToken.value,
queryClient,
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const client = useFetchClient();
const response = await client.getJSON<CountResult>('events/count', {
params: request.params,
signal
});

return response.data!;
},
queryKey: queryKeys.count()
}));
}

export function getEventQuery(request: GetEventRequest) {
return createQuery<PersistentEvent, ProblemDetails>(() => ({
enabled: () => !!accessToken.value && !!request.route.id,
Expand All @@ -111,19 +145,43 @@ export function getEventQuery(request: GetEventRequest) {
export function getProjectCountQuery(request: GetProjectCountRequest) {
const queryClient = useQueryClient();

return createQuery<PersistentEvent[], ProblemDetails>(() => ({
return createQuery<CountResult, ProblemDetails>(() => ({
enabled: () => !!accessToken.value && !!request.route.projectId,
queryClient,
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const client = useFetchClient();
const response = await client.getJSON<PersistentEvent[]>(`/projects/${request.route.projectId}/events/count`, {
const response = await client.getJSON<CountResult>(`/projects/${request.route.projectId}/events/count`, {
params: request.params,
signal
});

return response.data!;
},
queryKey: queryKeys.projects(request.route.projectId)
queryKey: queryKeys.projectsCount(request.route.projectId)
}));
}

export function getStackCountQuery(request: GetStackCountRequest) {
const queryClient = useQueryClient();

return createQuery<CountResult, ProblemDetails>(() => ({
enabled: () => !!accessToken.value && !!request.route.stackId,
queryClient,
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const client = useFetchClient();
const response = await client.getJSON<CountResult>('events/count', {
params: {
...request.params,
filter: request.params?.filter?.includes(`stack:${request.route.stackId}`)
? request.params.filter
: [request.params?.filter, `stack:${request.route.stackId}`].filter(Boolean).join(' ')
},
signal
});

return response.data!;
},
queryKey: queryKeys.stacksCount(request.route.stackId)
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import NumberFormatter from '$comp/formatters/Number.svelte';
import TimeAgo from '$comp/formatters/TimeAgo.svelte';
import { Checkbox } from '$comp/ui/checkbox';
import { nameof } from '$lib/utils';
import { DEFAULT_LIMIT } from '$shared/api.svelte';
import { DEFAULT_LIMIT } from '$shared/api/api.svelte';
import { persisted } from '$shared/persisted.svelte';
import {
type ColumnDef,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type {
BucketAggregate,
DateHistogramBucket,
ExtendedStatsAggregate,
IAggregate,
IBucket,
KeyedBucket,
MultiBucketAggregate,
ObjectValueAggregate,
PercentileItem,
PercentilesAggregate,
SingleBucketAggregate,
StatsAggregate,
TermsAggregate,
TopHitsAggregate,
ValueAggregate
} from '../models';

export function average(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate {
return tryGet<ValueAggregate>(aggregations, key);
}

export function cardinality(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate {
return tryGet<ValueAggregate>(aggregations, key);
}

export function dateHistogram(aggregations: Record<string, IAggregate> | undefined, key: string): MultiBucketAggregate<DateHistogramBucket> | undefined {
return getMultiBucketAggregate<DateHistogramBucket>(aggregations, key);
}

export function extendedStats(aggregations: Record<string, IAggregate> | undefined, key: string): ExtendedStatsAggregate | undefined {
return tryGet<ExtendedStatsAggregate>(aggregations, key);
}

export function geoHash(aggregations: Record<string, IAggregate> | undefined, key: string): MultiBucketAggregate<KeyedBucket<string>> | undefined {
return getMultiKeyedBucketAggregate<string>(aggregations, key);
}

export function getPercentile(agg: PercentilesAggregate, percentile: number): PercentileItem | undefined {
return agg.items.find((i) => i.percentile === percentile); // Checked
}

export function max<T = number>(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate<T> {
return tryGet<ValueAggregate<T>>(aggregations, key);
}

export function metric(aggregations: Record<string, IAggregate> | undefined, key: string): ObjectValueAggregate | undefined {
const valueMetric = tryGet<ValueAggregate>(aggregations, key);
if (valueMetric) {
return <ObjectValueAggregate>{
data: valueMetric.data,
value: valueMetric.value
};
}

return tryGet<ObjectValueAggregate>(aggregations, key);
}

export function min<T = number>(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate<T> {
return tryGet<ValueAggregate<T>>(aggregations, key);
}

export function missing(aggregations: Record<string, IAggregate> | undefined, key: string): SingleBucketAggregate | undefined {
return tryGet<SingleBucketAggregate>(aggregations, key);
}

export function percentiles(aggregations: Record<string, IAggregate> | undefined, key: string): PercentilesAggregate | undefined {
return tryGet<PercentilesAggregate>(aggregations, key);
}

export function stats(aggregations: Record<string, IAggregate> | undefined, key: string): StatsAggregate | undefined {
return tryGet<StatsAggregate>(aggregations, key);
}

export function sum(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate {
return tryGet<ValueAggregate>(aggregations, key);
}

export function terms<TKey = string>(aggregations: Record<string, IAggregate> | undefined, key: string): TermsAggregate<TKey> | undefined {
const bucket = tryGet<BucketAggregate>(aggregations, key);
if (!bucket) {
return;
}

return <TermsAggregate<TKey>>{
buckets: getKeyedBuckets<TKey>(bucket.items),
data: bucket.data
};
}

export function topHits<T = unknown>(aggregations: Record<string, IAggregate>): TopHitsAggregate<T> | undefined {
return tryGet<TopHitsAggregate<T>>(aggregations, 'tophits');
}

function getKeyedBuckets<TKey>(items: IBucket[]): KeyedBucket<TKey>[] {
return items
.filter((bucket): bucket is KeyedBucket<TKey> => 'key' in bucket)
.map((bucket) => ({
aggregations: bucket.aggregations,
key: bucket.key, // NOTE: May have to convert to proper type
key_as_string: bucket.key_as_string,
total: bucket.total
}));
}

function getMultiBucketAggregate<TBucket extends IBucket>(
aggregations: Record<string, IAggregate> | undefined,
key: string
): MultiBucketAggregate<TBucket> | undefined {
const bucket = tryGet<BucketAggregate>(aggregations, key);
if (!bucket) {
return;
}

return <MultiBucketAggregate<TBucket>>{
buckets: bucket.items,
data: bucket.data
};
}

function getMultiKeyedBucketAggregate<TKey>(
aggregations: Record<string, IAggregate> | undefined,
key: string
): MultiBucketAggregate<KeyedBucket<TKey>> | undefined {
const bucket = tryGet<BucketAggregate>(aggregations, key);
if (!bucket) {
return;
}

return <MultiBucketAggregate<KeyedBucket<TKey>>>{
buckets: getKeyedBuckets<TKey>(bucket.items),
data: bucket.data
};
}

function tryGet<TAggregate extends IAggregate>(aggregations: Record<string, IAggregate> | undefined, key: string): TAggregate | undefined {
return aggregations?.[key] as TAggregate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { IAggregate } from '.';

export interface AggregationsHelper {
aggregations: Record<string, IAggregate>;
}

export interface BucketAggregate extends IAggregate {
items: IBucket[];
total: number;
}

export interface BucketAggregateBase extends AggregationsHelper, IAggregate {
aggregations: Record<string, IAggregate>;
}

export interface BucketBase extends AggregationsHelper, IBucket {
aggregations: Record<string, IAggregate>;
}

export interface DateHistogramBucket extends KeyedBucket<number> {
date: string; // This needs to be converted to a date.
}

export interface ExtendedStatsAggregate extends StatsAggregate {
std_deviation?: number;
std_deviation_bounds?: StandardDeviationBounds;
sum_of_squares?: number;
variance?: number;
}

export interface IBucket {
data?: Record<string, unknown>;
}

export interface KeyedBucket<T> extends BucketBase {
key: T;
key_as_string: string;
total?: number;
}

export type MetricAggregateBase = IAggregate;

export interface MultiBucketAggregate<TBucket extends IBucket> extends BucketAggregateBase {
buckets: TBucket[];
}

export interface ObjectValueAggregate extends MetricAggregateBase {
value: unknown;
}

export interface PercentileItem {
percentile: number;
value?: number;
}

export interface PercentilesAggregate extends MetricAggregateBase {
items: PercentileItem[];
}

export interface RangeBucket extends BucketBase {
from?: number;
from_as_string?: string;
key: string;
to?: number;
to_as_string?: string;
total: number;
}

export interface SingleBucketAggregate extends BucketAggregateBase {
total: number;
}

export interface StandardDeviationBounds {
lower?: number;
upper?: number;
}

export interface StatsAggregate extends MetricAggregateBase {
average?: number;
count: number;
max?: number;
min?: number;
sum?: number;
}

export type TermsAggregate<TKey> = MultiBucketAggregate<KeyedBucket<TKey>>;

export interface TopHitsAggregate<T> extends MetricAggregateBase {
hits: T[];
max_score?: number;
total: number;
}

export interface ValueAggregate<T = number> extends MetricAggregateBase {
value: T;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { CountResult, type IAggregate } from '$generated/api';

export * from './aggregations';
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte';
import { getTableContext } from '$features/events/components/table/options.svelte';
import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models';
import { useFetchClientStatus } from '$shared/api.svelte';
import { useFetchClientStatus } from '$shared/api/api.svelte';
import { persisted } from '$shared/persisted.svelte';
import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient';
import { createTable } from '@tanstack/svelte-table';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Button } from '$comp/ui/button';
import { Separator } from '$comp/ui/separator';
import { User } from '$features/users/models';
import { useFetchClientStatus } from '$shared/api.svelte';
import { useFetchClientStatus } from '$shared/api/api.svelte';
import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
const data = $state(new User());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
microsoftClientId
} from '$features/auth/index.svelte';
import { User } from '$features/users/models';
import { useFetchClientStatus } from '$shared/api.svelte';
import { useFetchClientStatus } from '$shared/api/api.svelte';
import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
import IconFacebook from '~icons/mdi/facebook';
import IconGitHub from '~icons/mdi/github';
Expand Down
Loading

0 comments on commit 9fd7d34

Please sign in to comment.