A very performant and light (2mb in memory) link shortener and tracker. Written in Rust and React and uses Postgres/SQLite.

fix statistics; i need to make unit tests

Changed files
+47 -41
frontend
src
src
+15 -4
frontend/src/api/client.ts
···
};
export const getLinkClickStats = async (id: number) => {
-
const response = await api.get<ClickStats[]>(`/links/${id}/clicks`);
-
return response.data;
+
try {
+
const response = await api.get<ClickStats[]>(`/links/${id}/clicks`);
+
return response.data;
+
} catch (error) {
+
console.error('Error fetching click stats:', error);
+
throw error;
+
}
};
export const getLinkSourceStats = async (id: number) => {
-
const response = await api.get<SourceStats[]>(`/links/${id}/sources`);
-
return response.data;
+
try {
+
const response = await api.get<SourceStats[]>(`/links/${id}/sources`);
+
return response.data;
+
} catch (error) {
+
console.error('Error fetching source stats:', error);
+
throw error;
+
}
};
+
export const checkFirstUser = async () => {
const response = await api.get<{ isFirstUser: boolean }>('/auth/check-first-user');
+8 -2
frontend/src/components/StatisticsModal.tsx
···
ResponsiveContainer,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
import { toast } from "@/hooks/use-toast"
import { useState, useEffect } from "react";
import { getLinkClickStats, getLinkSourceStats } from '../api/client';
···
]);
setClicksOverTime(clicksData);
setSourcesData(sourcesData);
-
} catch (error) {
+
} catch (error: any) {
console.error("Failed to fetch statistics:", error);
+
toast({
+
variant: "destructive",
+
title: "Error",
+
description: error.response?.data || "Failed to load statistics",
+
});
} finally {
setLoading(false);
}
};
-
+
fetchData();
}
}, [isOpen, linkId]);
+23 -33
src/handlers.rs
···
) -> Result<impl Responder, AppError> {
let link_id = path.into_inner();
-
// Verify the link belongs to the user
+
// First verify the link belongs to the user
let link = match &state.db {
DatabasePool::Postgres(pool) => {
-
let mut tx = pool.begin().await?;
-
let link = sqlx::query_as::<Postgres, (i32,)>(
-
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
-
)
-
.bind(link_id)
-
.bind(user.user_id)
-
.fetch_optional(&mut *tx)
-
.await?;
-
tx.commit().await?;
-
link
+
sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = $1 AND user_id = $2")
+
.bind(link_id)
+
.bind(user.user_id)
+
.fetch_optional(pool)
+
.await?
}
DatabasePool::Sqlite(pool) => {
-
let mut tx = pool.begin().await?;
-
let link = sqlx::query_as::<Sqlite, (i32,)>(
-
"SELECT id FROM links WHERE id = ? AND user_id = ?",
-
)
-
.bind(link_id)
-
.bind(user.user_id)
-
.fetch_optional(&mut *tx)
-
.await?;
-
tx.commit().await?;
-
link
+
sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = ? AND user_id = ?")
+
.bind(link_id)
+
.bind(user.user_id)
+
.fetch_optional(pool)
+
.await?
}
};
···
let clicks = match &state.db {
DatabasePool::Postgres(pool) => {
-
sqlx::query_as::<Postgres, ClickStats>(
+
sqlx::query_as::<_, ClickStats>(
r#"
SELECT
-
DATE(created_at)::date as "date!",
-
COUNT(*)::bigint as "clicks!"
+
DATE(created_at) as date,
+
COUNT(*) as clicks
FROM clicks
WHERE link_id = $1
GROUP BY DATE(created_at)
···
.await?
}
DatabasePool::Sqlite(pool) => {
-
sqlx::query_as::<Sqlite, ClickStats>(
+
sqlx::query_as::<_, ClickStats>(
r#"
SELECT
-
DATE(created_at) as "date!",
-
COUNT(*) as "clicks!"
+
DATE(created_at) as date,
+
COUNT(*) as clicks
FROM clicks
WHERE link_id = ?
GROUP BY DATE(created_at)
···
let sources = match &state.db {
DatabasePool::Postgres(pool) => {
-
sqlx::query_as::<Postgres, SourceStats>(
+
sqlx::query_as::<_, SourceStats>(
r#"
SELECT
-
query_source as "source!",
-
COUNT(*)::bigint as "count!"
+
query_source as source, // Remove the ! mark
+
COUNT(*)::bigint as count // Remove the ! mark
FROM clicks
WHERE link_id = $1
AND query_source IS NOT NULL
···
.await?
}
DatabasePool::Sqlite(pool) => {
-
sqlx::query_as::<Sqlite, SourceStats>(
+
sqlx::query_as::<_, SourceStats>(
r#"
SELECT
-
query_source as "source!",
-
COUNT(*) as "count!"
+
query_source as source,
+
COUNT(*) as count
FROM clicks
WHERE link_id = ?
AND query_source IS NOT NULL
+1 -2
src/models.rs
···
use anyhow::Result;
-
use chrono::NaiveDate;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgRow;
···
#[derive(sqlx::FromRow, Serialize)]
pub struct ClickStats {
-
pub date: NaiveDate,
+
pub date: String,
pub clicks: i64,
}