mono/packages/ui/src/lib/db.ts
2026-02-25 10:11:54 +01:00

154 lines
5.6 KiB
TypeScript

import { queryClient } from './queryClient';
import { supabase as defaultSupabase } from "@/integrations/supabase/client";
import { SupabaseClient } from "@supabase/supabase-js";
// Deprecated: Caching now handled by React Query
// Keeping for backward compatibility
type CacheStorageType = 'memory' | 'local';
/**
* Parse a flat string key into a hierarchical query key array.
* This enables SSE cascade invalidation via StreamInvalidator:
* invalidateQueries({ queryKey: ['posts'] }) matches ['posts', 'abc123']
*/
const KEY_PREFIXES: [RegExp, (...groups: string[]) => string[]][] = [
[/^full-post-(.+)$/, (_, id) => ['posts', 'full', id]],
[/^post-(.+)$/, (_, id) => ['posts', id]],
[/^like-(.+)-(.+)$/, (_, uid, pid) => ['likes', uid, pid]],
[/^selected-versions-(.+)$/, (_, ids) => ['pictures', 'selected-versions', ids]],
[/^versions-(.+)$/, (_, rest) => ['pictures', 'versions', rest]],
[/^picture-(.+)$/, (_, id) => ['pictures', id]],
[/^pictures-(.+)$/, (_, rest) => ['pictures', rest]],
[/^user-page-(.+?)-(.+)$/, (_, uid, slug) => ['pages', 'user-page', uid, slug]],
[/^user-pages-(.+)$/, (_, uid) => ['pages', 'user-pages', uid]],
[/^page-details-(.+)$/, (_, id) => ['pages', 'details', id]],
[/^page-(.+)$/, (_, id) => ['pages', id]],
[/^pages-(.+)$/, (_, rest) => ['pages', rest]],
[/^profile-(.+)$/, (_, id) => ['users', id]],
[/^settings-(.+)$/, (_, id) => ['users', 'settings', id]],
[/^roles-(.+)$/, (_, id) => ['users', 'roles', id]],
[/^openai-(.+)$/, (_, id) => ['users', 'openai', id]],
[/^api-keys-(.+)$/, (_, id) => ['users', 'api-keys', id]],
[/^google-(.+)$/, (_, id) => ['users', 'google', id]],
[/^user-secrets-(.+)$/, (_, id) => ['users', 'secrets', id]],
[/^provider-(.+)-(.+)$/, (_, uid, prov) => ['users', 'provider', uid, prov]],
[/^type-(.+)$/, (_, id) => ['types', id]],
[/^types-(.+)$/, (_, rest) => ['types', rest]],
[/^i18n-(.+)$/, (_, rest) => ['i18n', rest]],
[/^acl-(.+?)-(.+)$/, (_, type, id) => ['acl', type, id]],
[/^layout-(.+)$/, (_, id) => ['layouts', id]],
[/^layouts-(.+)$/, (_, rest) => ['layouts', rest]],
];
export const parseQueryKey = (key: string): string[] => {
for (const [pattern, builder] of KEY_PREFIXES) {
const match = key.match(pattern);
if (match) return builder(...match);
}
return [key]; // fallback: wrap as single-element array
};
export const fetchWithDeduplication = async <T>(
key: string,
fetcher: () => Promise<T>,
timeout: number = 25000,
storage: CacheStorageType = 'local'
): Promise<T> => {
const queryKey = parseQueryKey(key);
return queryClient.fetchQuery({
queryKey,
queryFn: fetcher,
staleTime: timeout > 1 ? timeout : 1000 * 30,
});
};
export const invalidateCache = (key: string) => {
const queryKey = parseQueryKey(key);
queryClient.invalidateQueries({ queryKey });
};
export const invalidateServerCache = async (types: string[]) => {
// Explicit cache invalidation is handled by the server on mutation
// No need to call /api/cache/invalidate manually
console.debug('invalidateServerCache: Skipped manual invalidation for', types);
};
export const checkLikeStatus = async (userId: string, pictureId: string, client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`like-${userId}-${pictureId}`, async () => {
const { data, error } = await supabase
.from('likes')
.select('id')
.eq('user_id', userId)
.eq('picture_id', pictureId)
.maybeSingle();
if (error) throw error;
return !!data;
});
};
export const fetchAnalytics = async (options: { limit?: number, startDate?: string, endDate?: string } = {}) => {
const { data: sessionData } = await defaultSupabase.auth.getSession();
const token = sessionData.session?.access_token;
if (!token) throw new Error('No active session');
const headers: HeadersInit = {};
headers['Authorization'] = `Bearer ${token}`;
const params = new URLSearchParams();
if (options.limit) params.append('limit', String(options.limit));
if (options.startDate) params.append('startDate', options.startDate);
if (options.endDate) params.append('endDate', options.endDate);
// Server URL logic from fetchMediaItemsByIds
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const res = await fetch(`${serverUrl}/api/analytics?${params.toString()}`, { headers });
if (!res.ok) {
if (res.status === 403 || res.status === 401) {
throw new Error('Unauthorized');
}
throw new Error(`Failed to fetch analytics: ${res.statusText}`);
}
return await res.json();
};
export const clearAnalytics = async () => {
const { data: sessionData } = await defaultSupabase.auth.getSession();
const token = sessionData.session?.access_token;
if (!token) throw new Error('No active session');
const headers: HeadersInit = {};
headers['Authorization'] = `Bearer ${token}`;
// Server URL logic from fetchMediaItemsByIds
const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL;
const res = await fetch(`${serverUrl}/api/analytics`, {
method: 'DELETE',
headers
});
if (!res.ok) {
if (res.status === 403 || res.status === 401) {
throw new Error('Unauthorized');
}
throw new Error(`Failed to clear analytics: ${res.statusText}`);
}
return await res.json();
};