feat: Implement flexible media sorting by standard columns and metadata tags by refactoring backend query building and updating frontend API parameters.
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
|||||||
shareAlbum,
|
shareAlbum,
|
||||||
updateAlbum,
|
updateAlbum,
|
||||||
type AddMediaToAlbumPayload,
|
type AddMediaToAlbumPayload,
|
||||||
type CreateAlbumPayload,
|
|
||||||
type RemoveMediaFromAlbumPayload,
|
type RemoveMediaFromAlbumPayload,
|
||||||
type SetAlbumThumbnailPayload,
|
type SetAlbumThumbnailPayload,
|
||||||
type ShareAlbumPayload,
|
type ShareAlbumPayload,
|
||||||
@@ -150,7 +149,7 @@ export const useRemoveMediaFromAlbum = (albumId: string) => {
|
|||||||
* Mutation hook to share an album with another user.
|
* Mutation hook to share an album with another user.
|
||||||
*/
|
*/
|
||||||
export const useShareAlbum = (albumId: string) => {
|
export const useShareAlbum = (albumId: string) => {
|
||||||
const queryClient = useQueryClient();
|
// const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: ShareAlbumPayload) => shareAlbum(albumId, payload),
|
mutationFn: (payload: ShareAlbumPayload) => shareAlbum(albumId, payload),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -17,10 +17,17 @@ const MEDIA_KEY = ["media"];
|
|||||||
* Query hook to fetch a paginated list of all media.
|
* Query hook to fetch a paginated list of all media.
|
||||||
* This uses `useInfiniteQuery` for "load more" functionality.
|
* This uses `useInfiniteQuery` for "load more" functionality.
|
||||||
*/
|
*/
|
||||||
export const useGetMediaList = () => {
|
export const useGetMediaList = (
|
||||||
|
params: {
|
||||||
|
sort_by?: string;
|
||||||
|
order?: "asc" | "desc";
|
||||||
|
mime_type?: string;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: [MEDIA_KEY, "list"],
|
queryKey: [MEDIA_KEY, "list", params],
|
||||||
queryFn: ({ pageParam = 1 }) => getMediaList({ page: pageParam, limit: 20 }),
|
queryFn: ({ pageParam = 1 }) =>
|
||||||
|
getMediaList({ page: pageParam, limit: 20, ...params }),
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
return lastPage.has_next_page ? lastPage.page + 1 : undefined;
|
return lastPage.has_next_page ? lastPage.page + 1 : undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|||||||
@@ -3,16 +3,26 @@ import { createFileRoute } from "@tanstack/react-router";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AuthenticatedImage } from "@/components/media/authenticated-image";
|
import { AuthenticatedImage } from "@/components/media/authenticated-image";
|
||||||
import type { Media } from "@/domain/types";
|
import type { Media } from "@/domain/types";
|
||||||
import { useMemo, useState } from "react"; // Import useMemo
|
import { useMemo, useState } from "react";
|
||||||
import { MediaViewer } from "@/components/media/media-viewer";
|
import { MediaViewer } from "@/components/media/media-viewer";
|
||||||
import { groupMediaByDate } from "@/lib/date-utils"; // Import our new helper
|
import { groupMediaByDate } from "@/lib/date-utils";
|
||||||
import { parseISO } from "date-fns";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
export const Route = createFileRoute("/media/")({
|
export const Route = createFileRoute("/media/")({
|
||||||
component: MediaPage,
|
component: MediaPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
function MediaPage() {
|
function MediaPage() {
|
||||||
|
const [sortBy, setSortBy] = useState<string>("created_at");
|
||||||
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
|
const [mimeType, setMimeType] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -20,20 +30,16 @@ function MediaPage() {
|
|||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isFetchingNextPage,
|
isFetchingNextPage,
|
||||||
} = useGetMediaList();
|
} = useGetMediaList({
|
||||||
|
sort_by: sortBy,
|
||||||
|
order: sortOrder,
|
||||||
|
mime_type: mimeType === "all" ? undefined : mimeType,
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
|
const [selectedMedia, setSelectedMedia] = useState<Media | null>(null);
|
||||||
|
|
||||||
const allMedia = useMemo(
|
const allMedia = useMemo(
|
||||||
() =>
|
() => data?.pages.flatMap((page) => page.data) ?? [],
|
||||||
data?.pages
|
|
||||||
.flatMap((page) => page.data)
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Sort by date (newest first)
|
|
||||||
const dateA = a.date_taken ?? a.created_at;
|
|
||||||
const dateB = b.date_taken ?? b.created_at;
|
|
||||||
return parseISO(dateB).getTime() - parseISO(dateA).getTime();
|
|
||||||
}) ?? [],
|
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -46,8 +52,65 @@ function MediaPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<h1 className="text-3xl font-bold">All Photos</h1>
|
<h1 className="text-3xl font-bold">All Photos</h1>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="Sort by" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="created_at">Date Created</SelectItem>
|
||||||
|
<SelectItem value="date_taken">Date Taken</SelectItem>
|
||||||
|
<SelectItem value="original_filename">Filename</SelectItem>
|
||||||
|
<SelectItem value="custom">Custom Tag...</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{sortBy === "custom" && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter tag name"
|
||||||
|
className="border rounded px-2 py-1 text-sm w-[140px]"
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (e.target.value) setSortBy(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setSortBy(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={sortOrder}
|
||||||
|
onValueChange={(val) => setSortOrder(val as "asc" | "desc")}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="Order" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="desc">Newest First</SelectItem>
|
||||||
|
<SelectItem value="asc">Oldest First</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={mimeType ?? "all"}
|
||||||
|
onValueChange={(val) => setMimeType(val === "all" ? undefined : val)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue placeholder="Filter by Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Types</SelectItem>
|
||||||
|
<SelectItem value="image/jpeg">JPEG</SelectItem>
|
||||||
|
<SelectItem value="image/png">PNG</SelectItem>
|
||||||
|
<SelectItem value="video/mp4">MP4</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading && <p>Loading photos...</p>}
|
{isLoading && <p>Loading photos...</p>}
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import type { Media, MediaDetails, PaginatedResponse } from "@/domain/types"
|
|||||||
import apiClient from "@/services/api-client"
|
import apiClient from "@/services/api-client"
|
||||||
|
|
||||||
type MediaListParams = {
|
type MediaListParams = {
|
||||||
page: number
|
page: number;
|
||||||
limit: number
|
limit: number;
|
||||||
}
|
sort_by?: string;
|
||||||
|
order?: "asc" | "desc";
|
||||||
|
mime_type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const API_PREFIX = import.meta.env.VITE_PREFIX_PATH || '';
|
const API_PREFIX = import.meta.env.VITE_PREFIX_PATH || "";
|
||||||
|
|
||||||
export const processMediaUrls = (media: Media): Media => ({
|
export const processMediaUrls = (media: Media): Media => ({
|
||||||
...media,
|
...media,
|
||||||
@@ -22,9 +25,12 @@ export const processMediaUrls = (media: Media): Media => ({
|
|||||||
export const getMediaList = async ({
|
export const getMediaList = async ({
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
|
sort_by,
|
||||||
|
order,
|
||||||
|
mime_type,
|
||||||
}: MediaListParams): Promise<PaginatedResponse<Media>> => {
|
}: MediaListParams): Promise<PaginatedResponse<Media>> => {
|
||||||
const { data } = await apiClient.get("/media", {
|
const { data } = await apiClient.get("/media", {
|
||||||
params: { page, limit },
|
params: { page, limit, sort_by, order, mime_type },
|
||||||
});
|
});
|
||||||
|
|
||||||
data.data = data.data.map(processMediaUrls);
|
data.data = data.data.map(processMediaUrls);
|
||||||
|
|||||||
@@ -4,6 +4,98 @@ use libertas_core::{
|
|||||||
};
|
};
|
||||||
use sqlx::QueryBuilder as SqlxQueryBuilder;
|
use sqlx::QueryBuilder as SqlxQueryBuilder;
|
||||||
|
|
||||||
|
pub trait SortStrategy: Send + Sync {
|
||||||
|
fn can_handle(&self, column: &str) -> bool;
|
||||||
|
fn apply_join<'a>(
|
||||||
|
&self,
|
||||||
|
query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
|
column: &'a str,
|
||||||
|
) -> CoreResult<()>;
|
||||||
|
fn apply_sort<'a>(
|
||||||
|
&self,
|
||||||
|
query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
|
column: &'a str,
|
||||||
|
direction: &str,
|
||||||
|
) -> CoreResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StandardSortStrategy {
|
||||||
|
allowed_columns: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StandardSortStrategy {
|
||||||
|
pub fn new(allowed_columns: Vec<String>) -> Self {
|
||||||
|
Self { allowed_columns }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortStrategy for StandardSortStrategy {
|
||||||
|
fn can_handle(&self, column: &str) -> bool {
|
||||||
|
self.allowed_columns.contains(&column.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_join<'a>(
|
||||||
|
&self,
|
||||||
|
_query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
|
_column: &'a str,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_sort<'a>(
|
||||||
|
&self,
|
||||||
|
query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
|
column: &'a str,
|
||||||
|
direction: &str,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
let nulls_order = if direction == "ASC" {
|
||||||
|
"NULLS LAST"
|
||||||
|
} else {
|
||||||
|
"NULLS FIRST"
|
||||||
|
};
|
||||||
|
let order_by_clause = format!(" ORDER BY {} {} {}", column, direction, nulls_order);
|
||||||
|
query.push(order_by_clause);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MetadataSortStrategy;
|
||||||
|
|
||||||
|
impl SortStrategy for MetadataSortStrategy {
|
||||||
|
fn can_handle(&self, _column: &str) -> bool {
|
||||||
|
true // Handles everything else
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_join<'a>(
|
||||||
|
&self,
|
||||||
|
query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
|
column: &'a str,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
// Join with media_metadata to sort by tag value
|
||||||
|
query.push(" LEFT JOIN media_metadata sort_mm ON media.id = sort_mm.media_id AND sort_mm.tag_name = ");
|
||||||
|
query.push_bind(column);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_sort<'a>(
|
||||||
|
&self,
|
||||||
|
query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
|
_column: &'a str,
|
||||||
|
direction: &str,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
let nulls_order = if direction == "ASC" {
|
||||||
|
"NULLS LAST"
|
||||||
|
} else {
|
||||||
|
"NULLS FIRST"
|
||||||
|
};
|
||||||
|
|
||||||
|
let order_by_clause = format!(" ORDER BY sort_mm.tag_value {} {}", direction, nulls_order);
|
||||||
|
query.push(order_by_clause);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait QueryBuilder<T> {
|
pub trait QueryBuilder<T> {
|
||||||
fn apply_options_to_query<'a>(
|
fn apply_options_to_query<'a>(
|
||||||
&self,
|
&self,
|
||||||
@@ -13,28 +105,40 @@ pub trait QueryBuilder<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct MediaQueryBuilder {
|
pub struct MediaQueryBuilder {
|
||||||
allowed_sort_columns: Vec<String>,
|
sort_strategies: Vec<Box<dyn SortStrategy>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaQueryBuilder {
|
impl MediaQueryBuilder {
|
||||||
pub fn new(allowed_sort_columns: Vec<String>) -> Self {
|
pub fn new(sort_strategies: Vec<Box<dyn SortStrategy>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
allowed_sort_columns,
|
sort_strategies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_sort_column<'a>(&self, column: &'a str) -> CoreResult<&'a str> {
|
pub fn apply_joins<'a>(
|
||||||
if self.allowed_sort_columns.contains(&column.to_string()) {
|
&self,
|
||||||
Ok(column)
|
mut query: SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
} else {
|
options: &'a ListMediaOptions,
|
||||||
Err(CoreError::Validation(format!(
|
) -> CoreResult<SqlxQueryBuilder<'a, sqlx::Postgres>> {
|
||||||
"Sorting by '{}' is not supported",
|
if let Some(filter) = &options.filter {
|
||||||
column
|
if let Some(metadata_filters) = &filter.metadata_filters {
|
||||||
)))
|
if !metadata_filters.is_empty() {
|
||||||
|
query.push(" JOIN media_metadata mm ON media.id = mm.media_id ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_filters_to_query<'a>(
|
if let Some(sort) = &options.sort {
|
||||||
|
let strategy = self.sort_strategies.iter().find(|s| s.can_handle(&sort.sort_by));
|
||||||
|
if let Some(strategy) = strategy {
|
||||||
|
strategy.apply_join(&mut query, &sort.sort_by)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_conditions<'a>(
|
||||||
&self,
|
&self,
|
||||||
mut query: SqlxQueryBuilder<'a, sqlx::Postgres>,
|
mut query: SqlxQueryBuilder<'a, sqlx::Postgres>,
|
||||||
options: &'a ListMediaOptions,
|
options: &'a ListMediaOptions,
|
||||||
@@ -49,7 +153,6 @@ impl MediaQueryBuilder {
|
|||||||
if let Some(metadata_filters) = &filter.metadata_filters {
|
if let Some(metadata_filters) = &filter.metadata_filters {
|
||||||
if !metadata_filters.is_empty() {
|
if !metadata_filters.is_empty() {
|
||||||
metadata_filter_count = metadata_filters.len() as i64;
|
metadata_filter_count = metadata_filters.len() as i64;
|
||||||
query.push(" JOIN media_metadata mm ON media.id = mm.media_id ");
|
|
||||||
query.push(" AND ( ");
|
query.push(" AND ( ");
|
||||||
|
|
||||||
for (i, filter) in metadata_filters.iter().enumerate() {
|
for (i, filter) in metadata_filters.iter().enumerate() {
|
||||||
@@ -75,18 +178,19 @@ impl MediaQueryBuilder {
|
|||||||
options: &'a ListMediaOptions,
|
options: &'a ListMediaOptions,
|
||||||
) -> CoreResult<SqlxQueryBuilder<'a, sqlx::Postgres>> {
|
) -> CoreResult<SqlxQueryBuilder<'a, sqlx::Postgres>> {
|
||||||
if let Some(sort) = &options.sort {
|
if let Some(sort) = &options.sort {
|
||||||
let column = self.validate_sort_column(&sort.sort_by)?;
|
|
||||||
let direction = match sort.sort_order {
|
let direction = match sort.sort_order {
|
||||||
SortOrder::Asc => "ASC",
|
SortOrder::Asc => "ASC",
|
||||||
SortOrder::Desc => "DESC",
|
SortOrder::Desc => "DESC",
|
||||||
};
|
};
|
||||||
let nulls_order = if direction == "ASC" {
|
|
||||||
"NULLS LAST"
|
let strategy = self.sort_strategies.iter().find(|s| s.can_handle(&sort.sort_by));
|
||||||
|
|
||||||
|
if let Some(strategy) = strategy {
|
||||||
|
strategy.apply_sort(&mut query, &sort.sort_by, direction)?;
|
||||||
} else {
|
} else {
|
||||||
"NULLS FIRST"
|
// Should not happen if we have a default/catch-all strategy, but good to handle
|
||||||
};
|
return Err(CoreError::Validation(format!("No sort strategy found for column: {}", sort.sort_by)));
|
||||||
let order_by_clause = format!("ORDER BY {} {} {}", column, direction, nulls_order);
|
}
|
||||||
query.push(order_by_clause);
|
|
||||||
} else {
|
} else {
|
||||||
query.push(" ORDER BY media.created_at DESC NULLS LAST ");
|
query.push(" ORDER BY media.created_at DESC NULLS LAST ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,21 @@ pub struct PostgresMediaRepository {
|
|||||||
|
|
||||||
impl PostgresMediaRepository {
|
impl PostgresMediaRepository {
|
||||||
pub fn new(pool: PgPool, config: &AppConfig) -> Self {
|
pub fn new(pool: PgPool, config: &AppConfig) -> Self {
|
||||||
let allowed_columns = config
|
let mut allowed_columns = config
|
||||||
.allowed_sort_columns
|
.allowed_sort_columns
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| vec!["created_at".to_string(), "original_filename".to_string()]);
|
.unwrap_or_else(|| vec!["created_at".to_string(), "original_filename".to_string()]);
|
||||||
|
|
||||||
|
allowed_columns.push("date_taken".to_string());
|
||||||
|
|
||||||
|
let strategies: Vec<Box<dyn crate::query_builder::SortStrategy>> = vec![
|
||||||
|
Box::new(crate::query_builder::StandardSortStrategy::new(allowed_columns)),
|
||||||
|
Box::new(crate::query_builder::MetadataSortStrategy),
|
||||||
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
pool,
|
pool,
|
||||||
query_builder: Arc::new(MediaQueryBuilder::new(allowed_columns)),
|
query_builder: Arc::new(MediaQueryBuilder::new(strategies)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +114,15 @@ impl MediaRepository for PostgresMediaRepository {
|
|||||||
) -> CoreResult<(Vec<Media>, i64)> {
|
) -> CoreResult<(Vec<Media>, i64)> {
|
||||||
let count_base_sql = "SELECT COUNT(DISTINCT media.id) as total FROM media";
|
let count_base_sql = "SELECT COUNT(DISTINCT media.id) as total FROM media";
|
||||||
let mut count_query = sqlx::QueryBuilder::new(count_base_sql);
|
let mut count_query = sqlx::QueryBuilder::new(count_base_sql);
|
||||||
|
|
||||||
|
count_query = self.query_builder.apply_joins(count_query, options)?;
|
||||||
|
|
||||||
count_query.push(" WHERE media.owner_id = ");
|
count_query.push(" WHERE media.owner_id = ");
|
||||||
count_query.push_bind(user_id);
|
count_query.push_bind(user_id);
|
||||||
|
|
||||||
let (mut count_query, metadata_filter_count) = self
|
let (mut count_query, metadata_filter_count) = self
|
||||||
.query_builder
|
.query_builder
|
||||||
.apply_filters_to_query(count_query, options)?;
|
.apply_conditions(count_query, options)?;
|
||||||
|
|
||||||
if metadata_filter_count > 0 {
|
if metadata_filter_count > 0 {
|
||||||
count_query.push(" GROUP BY media.id ");
|
count_query.push(" GROUP BY media.id ");
|
||||||
@@ -133,12 +143,15 @@ impl MediaRepository for PostgresMediaRepository {
|
|||||||
|
|
||||||
let data_base_sql = "SELECT media.id, media.owner_id, media.storage_path, media.original_filename, media.mime_type, media.hash, media.created_at, media.thumbnail_path, media.date_taken FROM media";
|
let data_base_sql = "SELECT media.id, media.owner_id, media.storage_path, media.original_filename, media.mime_type, media.hash, media.created_at, media.thumbnail_path, media.date_taken FROM media";
|
||||||
let mut data_query = sqlx::QueryBuilder::new(data_base_sql);
|
let mut data_query = sqlx::QueryBuilder::new(data_base_sql);
|
||||||
|
|
||||||
|
data_query = self.query_builder.apply_joins(data_query, options)?;
|
||||||
|
|
||||||
data_query.push(" WHERE media.owner_id = ");
|
data_query.push(" WHERE media.owner_id = ");
|
||||||
data_query.push_bind(user_id);
|
data_query.push_bind(user_id);
|
||||||
|
|
||||||
let (mut data_query, metadata_filter_count) = self
|
let (mut data_query, metadata_filter_count) = self
|
||||||
.query_builder
|
.query_builder
|
||||||
.apply_filters_to_query(data_query, options)?;
|
.apply_conditions(data_query, options)?;
|
||||||
|
|
||||||
if metadata_filter_count > 0 {
|
if metadata_filter_count > 0 {
|
||||||
data_query.push(" GROUP BY media.id ");
|
data_query.push(" GROUP BY media.id ");
|
||||||
@@ -174,12 +187,15 @@ impl MediaRepository for PostgresMediaRepository {
|
|||||||
JOIN face_regions fr ON media.id = fr.media_id
|
JOIN face_regions fr ON media.id = fr.media_id
|
||||||
";
|
";
|
||||||
let mut count_query = sqlx::QueryBuilder::new(count_base_sql);
|
let mut count_query = sqlx::QueryBuilder::new(count_base_sql);
|
||||||
|
|
||||||
|
count_query = self.query_builder.apply_joins(count_query, options)?;
|
||||||
|
|
||||||
count_query.push(" WHERE fr.person_id = ");
|
count_query.push(" WHERE fr.person_id = ");
|
||||||
count_query.push_bind(person_id);
|
count_query.push_bind(person_id);
|
||||||
|
|
||||||
let (mut count_query, _metadata_filter_count) = self
|
let (mut count_query, _metadata_filter_count) = self
|
||||||
.query_builder
|
.query_builder
|
||||||
.apply_filters_to_query(count_query, options)?;
|
.apply_conditions(count_query, options)?;
|
||||||
|
|
||||||
let total_items_result = count_query
|
let total_items_result = count_query
|
||||||
.build_query_scalar()
|
.build_query_scalar()
|
||||||
@@ -195,12 +211,15 @@ impl MediaRepository for PostgresMediaRepository {
|
|||||||
JOIN face_regions fr ON media.id = fr.media_id
|
JOIN face_regions fr ON media.id = fr.media_id
|
||||||
";
|
";
|
||||||
let mut data_query = sqlx::QueryBuilder::new(data_base_sql);
|
let mut data_query = sqlx::QueryBuilder::new(data_base_sql);
|
||||||
|
|
||||||
|
data_query = self.query_builder.apply_joins(data_query, options)?;
|
||||||
|
|
||||||
data_query.push(" WHERE fr.person_id = ");
|
data_query.push(" WHERE fr.person_id = ");
|
||||||
data_query.push_bind(person_id);
|
data_query.push_bind(person_id);
|
||||||
|
|
||||||
let (mut data_query, _metadata_filter_count) = self
|
let (mut data_query, _metadata_filter_count) = self
|
||||||
.query_builder
|
.query_builder
|
||||||
.apply_filters_to_query(data_query, options)?;
|
.apply_conditions(data_query, options)?;
|
||||||
|
|
||||||
data_query.push(" GROUP BY media.id ");
|
data_query.push(" GROUP BY media.id ");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user