feat: major dependency upgrades, remove Redis, fix mongoose 9 types

Dependencies upgraded:
- svelte 5.38→5.55, @sveltejs/kit 2.37→2.56, adapter-node 5.3→5.5
- mongoose 8→9, sharp 0.33→0.34, typescript 5→6
- lucide-svelte → @lucide/svelte 1.7 (Svelte 5 native package)
- vite 7→8 with rolldown (build time 33s→14s)
- Removed terser (esbuild/oxc default minifier is 20-100x faster)

Infrastructure:
- Removed Redis/ioredis cache layer — MongoDB handles caching natively
- Deleted src/lib/server/cache.ts and all cache.get/set/invalidate usage
- Removed redis-cli from deploy workflow, Redis env vars from .env.example

Mongoose 9 migration:
- Replaced deprecated `new: true` with `returnDocument: 'after'` (16 files)
- Fixed strict query filter types for ObjectId/paymentId fields
- Fixed season param type (string→number) in recipe API
- Removed unused @ts-expect-error in WorkoutSession model
This commit is contained in:
2026-04-06 12:20:59 +02:00
parent 6f53fe3b7b
commit 1fa2e350d7
68 changed files with 981 additions and 1743 deletions
-4
View File
@@ -1,10 +1,6 @@
# Database Configuration # Database Configuration
MONGO_URL="mongodb://user:password@host:port/database?authSource=admin" MONGO_URL="mongodb://user:password@host:port/database?authSource=admin"
# Redis Cache Configuration (optional - falls back to direct DB queries if unavailable)
REDIS_HOST="localhost" # Redis server hostname
REDIS_PORT="6379" # Redis server port
# Authentication Secrets (runtime only - not embedded in build) # Authentication Secrets (runtime only - not embedded in build)
AUTHENTIK_ID="your-authentik-client-id" AUTHENTIK_ID="your-authentik-client-id"
AUTHENTIK_SECRET="your-authentik-client-secret" AUTHENTIK_SECRET="your-authentik-client-secret"
-1
View File
@@ -33,7 +33,6 @@ jobs:
git reset --hard origin/master git reset --hard origin/master
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm run build pnpm run build
redis-cli KEYS 'recipes:*' | xargs -r redis-cli DEL
sudo systemctl stop homepage.service sudo systemctl stop homepage.service
mkdir -p dist mkdir -p dist
rm -rf dist/* rm -rf dist/*
+18 -20
View File
@@ -25,43 +25,41 @@
"packageManager": "pnpm@9.0.0", "packageManager": "pnpm@9.0.0",
"devDependencies": { "devDependencies": {
"@playwright/test": "1.56.1", "@playwright/test": "1.56.1",
"@sveltejs/adapter-auto": "^6.1.0", "@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.37.0", "@sveltejs/kit": "^2.56.1",
"@sveltejs/vite-plugin-svelte": "^6.1.3", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tauri-apps/cli": "^2.10.1", "@tauri-apps/cli": "^2.10.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.2.9", "@testing-library/svelte": "^5.3.1",
"@types/leaflet": "^1.9.21", "@types/leaflet": "^1.9.21",
"@types/node": "^22.12.0", "@types/node": "^22.12.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@vitest/ui": "^4.0.10", "@vitest/ui": "^4.1.2",
"jsdom": "^27.2.0", "jsdom": "^27.2.0",
"svelte": "^5.38.6", "svelte": "^5.55.1",
"svelte-check": "^4.0.0", "svelte-check": "^4.4.6",
"terser": "^5.46.0", "tslib": "^2.8.1",
"tslib": "^2.6.0", "typescript": "^6.0.2",
"typescript": "^5.1.6", "vite": "^8.0.4",
"vite": "^7.1.3", "vite-node": "^6.0.0",
"vite-node": "^5.3.0", "vitest": "^4.1.2"
"vitest": "^4.0.10"
}, },
"dependencies": { "dependencies": {
"@auth/sveltekit": "^1.11.1", "@auth/sveltekit": "^1.11.1",
"@huggingface/transformers": "^4.0.0", "@huggingface/transformers": "^4.0.1",
"@lucide/svelte": "^1.7.0",
"@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
"@sveltejs/adapter-node": "^5.0.0", "@sveltejs/adapter-node": "^5.5.4",
"@tauri-apps/plugin-geolocation": "^2.3.2", "@tauri-apps/plugin-geolocation": "^2.3.2",
"barcode-detector": "^3.1.2", "barcode-detector": "^3.1.2",
"chart.js": "^4.5.0", "chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"ioredis": "^5.9.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lucide-svelte": "^0.575.0", "mongoose": "^9.4.1",
"mongoose": "^8.0.0",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"sharp": "^0.33.0" "sharp": "^0.34.5"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
+871 -1123
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -144,7 +144,7 @@ dependencies = [
[[package]] [[package]]
name = "bocken" name = "bocken"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
+1 -1
View File
@@ -1,6 +1,6 @@
<script> <script>
import { themeStore } from '$lib/stores/theme.svelte'; import { themeStore } from '$lib/stores/theme.svelte';
import { Sun, Moon, SunMoon } from 'lucide-svelte'; import { Sun, Moon, SunMoon } from '@lucide/svelte';
</script> </script>
<style> <style>
+1 -1
View File
@@ -1,5 +1,5 @@
<script> <script>
import { X } from 'lucide-svelte'; import { X } from '@lucide/svelte';
import { getToasts } from '$lib/js/toast.svelte'; import { getToasts } from '$lib/js/toast.svelte';
const toasts = getToasts(); const toasts = getToasts();
@@ -2,7 +2,7 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { getAngelusStreak, getCurrentTimeSlot, type TimeSlot } from '$lib/stores/angelusStreak.svelte'; import { getAngelusStreak, getCurrentTimeSlot, type TimeSlot } from '$lib/stores/angelusStreak.svelte';
import StreakAura from '$lib/components/faith/StreakAura.svelte'; import StreakAura from '$lib/components/faith/StreakAura.svelte';
import { Coffee, Sun, Moon } from 'lucide-svelte'; import { Coffee, Sun, Moon } from '@lucide/svelte';
import { tick, onMount } from 'svelte'; import { tick, onMount } from 'svelte';
let burst = $state(false); let burst = $state(false);
@@ -1,6 +1,6 @@
<script> <script>
import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises'; import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises';
import { Search, X } from 'lucide-svelte'; import { Search, X } from '@lucide/svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
+1 -1
View File
@@ -1,7 +1,7 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { Heart, ExternalLink, ScanBarcode, X } from 'lucide-svelte'; import { Heart, ExternalLink, ScanBarcode, X } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
/** /**
@@ -1,7 +1,7 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises'; import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
import { Clock, Weight, Trophy, Route, Gauge, Flame } from 'lucide-svelte'; import { Clock, Weight, Trophy, Route, Gauge, Flame } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname)); const lang = $derived(detectFitnessLang($page.url.pathname));
+1 -1
View File
@@ -1,5 +1,5 @@
<script> <script>
import { Check, X } from 'lucide-svelte'; import { Check, X } from '@lucide/svelte';
import { METRIC_LABELS } from '$lib/data/exercises'; import { METRIC_LABELS } from '$lib/data/exercises';
import RestTimer from './RestTimer.svelte'; import RestTimer from './RestTimer.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -1,5 +1,5 @@
<script> <script>
import { Cloud, CloudOff, RefreshCw, AlertTriangle } from 'lucide-svelte'; import { Cloud, CloudOff, RefreshCw, AlertTriangle } from '@lucide/svelte';
/** @type {{ status: string }} */ /** @type {{ status: string }} */
let { status } = $props(); let { status } = $props();
@@ -1,6 +1,6 @@
<script> <script>
import { getExerciseById } from '$lib/data/exercises'; import { getExerciseById } from '$lib/data/exercises';
import { EllipsisVertical, MapPin } from 'lucide-svelte'; import { EllipsisVertical, MapPin } from '@lucide/svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
+1 -1
View File
@@ -1,6 +1,6 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { Play, Pause } from 'lucide-svelte'; import { Play, Pause } from '@lucide/svelte';
import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte'; import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
@@ -1,5 +1,5 @@
<script> <script>
import { UtensilsCrossed, X } from 'lucide-svelte'; import { UtensilsCrossed, X } from '@lucide/svelte';
import { toast } from '$lib/js/toast.svelte'; import { toast } from '$lib/js/toast.svelte';
let { let {
@@ -1,5 +1,5 @@
<script> <script>
import { ChevronLeft, ChevronRight } from 'lucide-svelte'; import { ChevronLeft, ChevronRight } from '@lucide/svelte';
import { getStickerById } from '$lib/utils/stickers'; import { getStickerById } from '$lib/utils/stickers';
import { import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
+1 -1
View File
@@ -1,6 +1,6 @@
<script> <script>
import { X, Sparkles, Wind, Bath, UtensilsCrossed, CookingPot, WashingMachine, import { X, Sparkles, Wind, Bath, UtensilsCrossed, CookingPot, WashingMachine,
Flower2, Droplets, Leaf, ShoppingCart, Trash2, Shirt, Brush } from 'lucide-svelte'; Flower2, Droplets, Leaf, ShoppingCart, Trash2, Shirt, Brush } from '@lucide/svelte';
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import Toggle from '$lib/components/Toggle.svelte'; import Toggle from '$lib/components/Toggle.svelte';
-342
View File
@@ -1,342 +0,0 @@
import Redis from 'ioredis';
// Key prefix for namespace isolation
const KEY_PREFIX = 'homepage:';
// Redis client configuration
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
// Reconnection strategy: exponential backoff with max 2 seconds
retryStrategy: (times) => Math.min(times * 50, 2000),
// Lazy connect to avoid blocking startup
lazyConnect: true,
// Connection timeout
connectTimeout: 10000,
// Enable offline queue to buffer commands during reconnection
enableOfflineQueue: true,
});
// Track connection status
let isConnected = false;
let isConnecting = false;
// Graceful connection with error handling
async function ensureConnection(): Promise<boolean> {
if (isConnected) {
return true;
}
if (isConnecting) {
// Wait for ongoing connection attempt
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (isConnected || !isConnecting) {
clearInterval(checkInterval);
resolve(isConnected);
}
}, 100);
});
}
isConnecting = true;
try {
await redis.connect();
isConnected = true;
console.log('[Redis] Connected successfully');
return true;
} catch (err) {
console.error('[Redis] Connection failed:', err);
isConnected = false;
return false;
} finally {
isConnecting = false;
}
}
// Handle connection events
redis.on('connect', () => {
isConnected = true;
console.log('[Redis] Connected');
});
redis.on('ready', () => {
isConnected = true;
console.log('[Redis] Ready');
});
redis.on('error', (err) => {
console.error('[Redis] Error:', err);
});
redis.on('close', () => {
isConnected = false;
console.log('[Redis] Connection closed');
});
redis.on('reconnecting', () => {
console.log('[Redis] Reconnecting...');
});
// Helper function to add prefix to keys
function prefixKey(key: string): string {
return `${KEY_PREFIX}${key}`;
}
// Helper function to add prefix to multiple keys
function prefixKeys(keys: string[]): string[] {
return keys.map(prefixKey);
}
/**
* Cache wrapper with automatic key prefixing and error handling
*/
export const cache = {
/**
* Get a value from cache
*/
async get(key: string): Promise<string | null> {
if (!(await ensureConnection())) {
return null;
}
try {
return await redis.get(prefixKey(key));
} catch (err) {
console.error(`[Redis] GET error for key "${key}":`, err);
return null;
}
},
/**
* Set a value in cache with optional TTL (in seconds)
*/
async set(key: string, value: string, ttl?: number): Promise<boolean> {
if (!(await ensureConnection())) {
return false;
}
try {
if (ttl) {
await redis.setex(prefixKey(key), ttl, value);
} else {
await redis.set(prefixKey(key), value);
}
return true;
} catch (err) {
console.error(`[Redis] SET error for key "${key}":`, err);
return false;
}
},
/**
* Delete one or more keys from cache
*/
async del(...keys: string[]): Promise<number> {
if (!(await ensureConnection())) {
return 0;
}
try {
const prefixedKeys = prefixKeys(keys);
return await redis.del(...prefixedKeys);
} catch (err) {
console.error(`[Redis] DEL error for keys "${keys.join(', ')}":`, err);
return 0;
}
},
/**
* Delete all keys matching a pattern (uses SCAN for safety)
* Pattern should NOT include the prefix (it will be added automatically)
*/
async delPattern(pattern: string): Promise<number> {
if (!(await ensureConnection())) {
return 0;
}
try {
const prefixedPattern = prefixKey(pattern);
const keys: string[] = [];
let cursor = '0';
// Use SCAN to safely iterate through keys
do {
const [nextCursor, matchedKeys] = await redis.scan(
cursor,
'MATCH',
prefixedPattern,
'COUNT',
100
);
cursor = nextCursor;
keys.push(...matchedKeys);
} while (cursor !== '0');
if (keys.length > 0) {
return await redis.del(...keys);
}
return 0;
} catch (err) {
console.error(`[Redis] DEL PATTERN error for pattern "${pattern}":`, err);
return 0;
}
},
/**
* Redis Set operations for managing sets (e.g., user favorites)
*/
sets: {
/**
* Add members to a set
*/
async add(key: string, ...members: string[]): Promise<number> {
if (!(await ensureConnection())) {
return 0;
}
try {
return await redis.sadd(prefixKey(key), ...members);
} catch (err) {
console.error(`[Redis] SADD error for key "${key}":`, err);
return 0;
}
},
/**
* Remove members from a set
*/
async remove(key: string, ...members: string[]): Promise<number> {
if (!(await ensureConnection())) {
return 0;
}
try {
return await redis.srem(prefixKey(key), ...members);
} catch (err) {
console.error(`[Redis] SREM error for key "${key}":`, err);
return 0;
}
},
/**
* Get all members of a set
*/
async members(key: string): Promise<string[]> {
if (!(await ensureConnection())) {
return [];
}
try {
return await redis.smembers(prefixKey(key));
} catch (err) {
console.error(`[Redis] SMEMBERS error for key "${key}":`, err);
return [];
}
},
/**
* Check if a member exists in a set
*/
async isMember(key: string, member: string): Promise<boolean> {
if (!(await ensureConnection())) {
return false;
}
try {
const result = await redis.sismember(prefixKey(key), member);
return result === 1;
} catch (err) {
console.error(`[Redis] SISMEMBER error for key "${key}":`, err);
return false;
}
},
},
/**
* Get cache statistics
*/
async getStats(): Promise<{ hits: number; misses: number; hitRate: string } | null> {
if (!(await ensureConnection())) {
return null;
}
try {
const info = await redis.info('stats');
const hitsMatch = info.match(/keyspace_hits:(\d+)/);
const missesMatch = info.match(/keyspace_misses:(\d+)/);
const hits = hitsMatch ? parseInt(hitsMatch[1]) : 0;
const misses = missesMatch ? parseInt(missesMatch[1]) : 0;
const total = hits + misses;
const hitRate = total > 0 ? ((hits / total) * 100).toFixed(2) : '0.00';
return { hits, misses, hitRate: `${hitRate}%` };
} catch (err) {
console.error('[Redis] Error getting stats:', err);
return null;
}
},
};
// Graceful shutdown
process.on('SIGTERM', () => {
redis.quit();
});
process.on('SIGINT', () => {
redis.quit();
});
/**
* Helper function to invalidate all recipe caches
* Call this after recipe create/update/delete operations
*/
export async function invalidateRecipeCaches(): Promise<void> {
try {
// Clear all recipe-related caches for both languages in parallel
await Promise.all([
cache.delPattern('recipes:rezepte:*'),
cache.delPattern('recipes:recipes:*'),
]);
} catch (err) {
console.error('[Cache] Error invalidating recipe caches:', err);
}
}
/**
* Helper function to invalidate cospend caches for specific users and/or payments
* Call this after payment create/update/delete operations
* @param usernames - Array of usernames whose caches should be invalidated
* @param paymentId - Optional payment ID to invalidate specific payment cache
*/
export async function invalidateCospendCaches(usernames: string[], paymentId?: string): Promise<void> {
try {
const invalidations: Promise<unknown>[] = [];
// Invalidate balance and debts caches for all affected users
for (const username of usernames) {
invalidations.push(
cache.del(`cospend:balance:${username}`),
cache.del(`cospend:debts:${username}`)
);
}
// Invalidate global balance cache
invalidations.push(cache.del('cospend:balance:all'));
// Invalidate payment list caches (all pagination variants)
invalidations.push(cache.delPattern('cospend:payments:list:*'));
// If specific payment ID provided, invalidate its cache
if (paymentId) {
invalidations.push(cache.del(`cospend:payment:${paymentId}`));
}
await Promise.all(invalidations);
} catch (err) {
console.error('[Cache] Error invalidating cospend caches:', err);
}
}
export default cache;
+1 -1
View File
@@ -100,7 +100,7 @@ class RecurringPaymentScheduler {
amount: split.amount, amount: split.amount,
proportion: split.proportion, proportion: split.proportion,
personalAmount: split.personalAmount personalAmount: split.personalAmount
}); } as any);
}); });
await Promise.all(splitPromises); await Promise.all(splitPromises);
-1
View File
@@ -246,5 +246,4 @@ const WorkoutSessionSchema = new mongoose.Schema(
WorkoutSessionSchema.index({ createdBy: 1, startTime: -1 }); WorkoutSessionSchema.index({ createdBy: 1, startTime: -1 });
WorkoutSessionSchema.index({ templateId: 1 }); WorkoutSessionSchema.index({ templateId: 1 });
// @ts-expect-error Mongoose model() produces a union type too complex for TS
export const WorkoutSession: mongoose.Model<IWorkoutSession> = mongoose.models.WorkoutSession || mongoose.model("WorkoutSession", WorkoutSessionSchema); export const WorkoutSession: mongoose.Model<IWorkoutSession> = mongoose.models.WorkoutSession || mongoose.model("WorkoutSession", WorkoutSessionSchema);
@@ -1,5 +1,5 @@
<script> <script>
import { ArrowDown, ArrowLeft } from 'lucide-svelte'; import { ArrowDown, ArrowLeft } from '@lucide/svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
let expanded = $state(null); let expanded = $state(null);
const isGerman = $derived($page.url.pathname.startsWith('/glaube')); const isGerman = $derived($page.url.pathname.startsWith('/glaube'));
@@ -45,7 +45,7 @@ onNavigate((navigation) => {
import UserHeader from '$lib/components/UserHeader.svelte'; import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte'; import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import OfflineSyncButton from '$lib/components/OfflineSyncButton.svelte'; import OfflineSyncButton from '$lib/components/OfflineSyncButton.svelte';
import { BookOpen, Heart, Leaf, LayoutGrid, Palette, Tag } from 'lucide-svelte'; import { BookOpen, Heart, Leaf, LayoutGrid, Palette, Tag } from '@lucide/svelte';
let { data, children } = $props(); let { data, children } = $props();
let user = $derived(data.session?.user); let user = $derived(data.session?.user);
@@ -2,7 +2,6 @@ import { redirect, fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { invalidateRecipeCaches } from '$lib/server/cache';
import { IMAGE_DIR } from '$env/static/private'; import { IMAGE_DIR } from '$env/static/private';
import { processAndSaveRecipeImage } from '$utils/imageProcessing'; import { processAndSaveRecipeImage } from '$utils/imageProcessing';
import { import {
@@ -110,9 +109,6 @@ export const actions = {
try { try {
await Recipe.create(recipe_json); await Recipe.create(recipe_json);
// Invalidate recipe caches after successful creation
await invalidateRecipeCaches();
// Redirect to the new recipe page // Redirect to the new recipe page
throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`); throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`);
} catch (dbError: unknown) { } catch (dbError: unknown) {
@@ -3,7 +3,6 @@ import { redirect, fail } from "@sveltejs/kit";
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { NutritionOverwrite } from '$models/NutritionOverwrite'; import { NutritionOverwrite } from '$models/NutritionOverwrite';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { invalidateRecipeCaches } from '$lib/server/cache';
import { invalidateOverwriteCache } from '$lib/server/nutritionMatcher'; import { invalidateOverwriteCache } from '$lib/server/nutritionMatcher';
import { IMAGE_DIR } from '$env/static/private'; import { IMAGE_DIR } from '$env/static/private';
import { rename, access, unlink } from 'fs/promises'; import { rename, access, unlink } from 'fs/promises';
@@ -200,7 +199,7 @@ export const actions = {
const result = await Recipe.findOneAndUpdate( const result = await Recipe.findOneAndUpdate(
{ short_name: originalShortName }, { short_name: originalShortName },
recipe_json, recipe_json,
{ new: true } { returnDocument: 'after' }
); );
if (!result) { if (!result) {
@@ -249,9 +248,6 @@ export const actions = {
} }
} }
// Invalidate recipe caches after successful update
await invalidateRecipeCaches();
// Redirect to the updated recipe page (might have new short_name) // Redirect to the updated recipe page (might have new short_name)
throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`); throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`);
} catch (dbError: unknown) { } catch (dbError: unknown) {
@@ -2,7 +2,6 @@ import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now // header: use for bearer token for now
// recipe json in body // recipe json in body
export const POST: RequestHandler = async ({request, cookies, locals}) => { export const POST: RequestHandler = async ({request, cookies, locals}) => {
@@ -20,8 +19,6 @@ export const POST: RequestHandler = async ({request, cookies, locals}) => {
await dbConnect(); await dbConnect();
try{ try{
await Recipe.create(recipe_json); await Recipe.create(recipe_json);
// Invalidate recipe caches after successful creation
await invalidateRecipeCaches();
} catch(e){ } catch(e){
throw error(400, e instanceof Error ? e.message : String(e)) throw error(400, e instanceof Error ? e.message : String(e))
} }
@@ -4,7 +4,6 @@ import { UserFavorites } from '$models/UserFavorites';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import type {RecipeModelType} from '$types/types'; import type {RecipeModelType} from '$types/types';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now // header: use for bearer token for now
// recipe json in body // recipe json in body
export const POST: RequestHandler = async ({request, locals}) => { export const POST: RequestHandler = async ({request, locals}) => {
@@ -70,9 +69,6 @@ export const POST: RequestHandler = async ({request, locals}) => {
// Delete the recipe // Delete the recipe
await Recipe.findOneAndDelete({short_name: short_name}); await Recipe.findOneAndDelete({short_name: short_name});
// Invalidate recipe caches after successful deletion
await invalidateRecipeCaches();
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{ return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
status: 200, status: 200,
}); });
@@ -6,7 +6,6 @@ import { error } from '@sveltejs/kit';
import { rename } from 'fs/promises'; import { rename } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now // header: use for bearer token for now
// recipe json in body // recipe json in body
@@ -48,9 +47,6 @@ export const POST: RequestHandler = async ({request, locals}) => {
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json); await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
// Invalidate recipe caches after successful update
await invalidateRecipeCaches();
return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{ return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{
status: 200, status: 200,
}); });
@@ -53,7 +53,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await UserFavorites.findOneAndUpdate( await UserFavorites.findOneAndUpdate(
{ username: session.user.nickname }, { username: session.user.nickname },
{ $addToSet: { favorites: recipe._id } }, { $addToSet: { favorites: recipe._id } },
{ upsert: true, new: true } { upsert: true, returnDocument: 'after' }
); );
@@ -28,7 +28,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const en = isEnglish(params.recipeLang!); const en = isEnglish(params.recipeLang!);
let recipes = await Recipe.find({ let recipes = await Recipe.find({
_id: { $in: userFavorites.favorites }, _id: { $in: userFavorites.favorites } as any,
...approvalFilter ...approvalFilter
}).lean() as unknown as RecipeModelType[]; }).lean() as unknown as RecipeModelType[];
@@ -1,26 +1,15 @@
import { json, type RequestHandler } from '@sveltejs/kit'; import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers'; import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => { export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!); const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
const cacheKey = `recipes:${params.recipeLang}:all_brief`;
let recipes: BriefRecipeType[] | null = null; await dbConnect();
const cached = await cache.get(cacheKey); const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
if (cached) { return json(JSON.parse(JSON.stringify(rand_array(recipes))));
recipes = JSON.parse(cached);
} else {
await dbConnect();
const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
return json(JSON.parse(JSON.stringify(rand_array(recipes!))));
}; };
@@ -2,27 +2,17 @@ import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers'; import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => { export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!); const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
const cacheKey = `recipes:${params.recipeLang}:in_season:${params.month}`;
let recipes = null; await dbConnect();
const cached = await cache.get(cacheKey); const dbRecipes = await Recipe.find(
{ season: parseInt(params.month!, 10), icon: { $ne: "🍽️" }, ...approvalFilter },
if (cached) { projection
recipes = JSON.parse(cached); ).lean();
} else { const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
await dbConnect();
const dbRecipes = await Recipe.find(
{ season: params.month, icon: { $ne: "🍽️" }, ...approvalFilter },
projection
).lean();
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
return json(JSON.parse(JSON.stringify(rand_array(recipes)))); return json(JSON.parse(JSON.stringify(rand_array(recipes))));
}; };
@@ -2,27 +2,17 @@ import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize'; import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers'; import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => { export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!); const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
const cacheKey = `recipes:${params.recipeLang}:tag:${params.tag}`;
let recipes = null; await dbConnect();
const cached = await cache.get(cacheKey); const dbRecipes = await Recipe.find(
{ [`${prefix}tags`]: params.tag, ...approvalFilter },
if (cached) { projection
recipes = JSON.parse(cached); ).lean();
} else { const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
await dbConnect();
const dbRecipes = await Recipe.find(
{ [`${prefix}tags`]: params.tag, ...approvalFilter },
projection
).lean();
recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
await cache.set(cacheKey, JSON.stringify(recipes), 3600);
}
return json(JSON.parse(JSON.stringify(rand_array(recipes)))); return json(JSON.parse(JSON.stringify(rand_array(recipes))));
}; };
@@ -81,7 +81,7 @@ export const PATCH: RequestHandler = async ({ request, locals }) => {
links: links.filter((l: any) => l.url?.trim()), links: links.filter((l: any) => l.url?.trim()),
notes: notes?.trim() || '' notes: notes?.trim() || ''
}, },
{ new: true } { returnDocument: 'after' }
).lean(); ).lean();
if (!item) { if (!item) {
+1 -24
View File
@@ -3,7 +3,6 @@ import { PaymentSplit } from '$models/PaymentSplit';
import { Payment } from '$models/Payment'; // Need to import Payment for populate to work import { Payment } from '$models/Payment'; // Need to import Payment for populate to work
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({ locals, url }) => { export const GET: RequestHandler = async ({ locals, url }) => {
const auth = await locals.auth(); const auth = await locals.auth();
@@ -18,14 +17,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
try { try {
if (includeAll) { if (includeAll) {
// Try cache first for all balances
const cacheKey = 'cospend:balance:all';
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const allSplits = await PaymentSplit.aggregate([ const allSplits = await PaymentSplit.aggregate([
{ {
$group: { $group: {
@@ -58,20 +49,9 @@ export const GET: RequestHandler = async ({ locals, url }) => {
allBalances: allSplits allBalances: allSplits
}; };
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result); return json(result);
} else { } else {
// Try cache first for individual user balance
const cacheKey = `cospend:balance:${username}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const userSplits = await PaymentSplit.find({ username }).lean(); const userSplits = await PaymentSplit.find({ username }).lean();
// Calculate net balance: negative = you are owed money, positive = you owe money // Calculate net balance: negative = you are owed money, positive = you owe money
@@ -112,9 +92,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
recentSplits recentSplits
}; };
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result); return json(result);
} }
@@ -122,4 +99,4 @@ export const GET: RequestHandler = async ({ locals, url }) => {
console.error('Error calculating balance:', e); console.error('Error calculating balance:', e);
throw error(500, 'Failed to calculate balance'); throw error(500, 'Failed to calculate balance');
} }
}; };
+2 -16
View File
@@ -3,7 +3,6 @@ import { PaymentSplit } from '$models/PaymentSplit';
import type { IPayment } from '$models/Payment'; import type { IPayment } from '$models/Payment';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import cache from '$lib/server/cache';
type PopulatedPayment = IPayment & { _id: import('mongoose').Types.ObjectId }; type PopulatedPayment = IPayment & { _id: import('mongoose').Types.ObjectId };
@@ -30,14 +29,6 @@ export const GET: RequestHandler = async ({ locals }) => {
await dbConnect(); await dbConnect();
try { try {
// Try cache first
const cacheKey = `cospend:debts:${currentUser}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
// Get all splits for the current user // Get all splits for the current user
const userSplits = await PaymentSplit.find({ username: currentUser }) const userSplits = await PaymentSplit.find({ username: currentUser })
.populate('paymentId') .populate('paymentId')
@@ -46,7 +37,7 @@ export const GET: RequestHandler = async ({ locals }) => {
// Get all other users who have splits with payments involving the current user // Get all other users who have splits with payments involving the current user
const paymentIds = userSplits.map(split => (split.paymentId as unknown as PopulatedPayment)._id); const paymentIds = userSplits.map(split => (split.paymentId as unknown as PopulatedPayment)._id);
const allRelatedSplits = await PaymentSplit.find({ const allRelatedSplits = await PaymentSplit.find({
paymentId: { $in: paymentIds }, paymentId: { $in: paymentIds } as any,
username: { $ne: currentUser } username: { $ne: currentUser }
}) })
.populate('paymentId') .populate('paymentId')
@@ -115,15 +106,10 @@ export const GET: RequestHandler = async ({ locals }) => {
totalIOwe: whoIOwe.reduce((sum, debt) => sum + debt.netAmount, 0) totalIOwe: whoIOwe.reduce((sum, debt) => sum + debt.netAmount, 0)
}; };
// Cache for 15 minutes (as suggested in plan for debt breakdown)
await cache.set(cacheKey, JSON.stringify(result), 900);
return json(result); return json(result);
} catch (e) { } catch (e) {
console.error('Error calculating debt breakdown:', e); console.error('Error calculating debt breakdown:', e);
throw error(500, 'Failed to calculate debt breakdown'); throw error(500, 'Failed to calculate debt breakdown');
} finally {
// Connection will be reused
} }
}; };
+8 -30
View File
@@ -4,7 +4,6 @@ import { PaymentSplit } from '$models/PaymentSplit';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { convertToCHF, isValidCurrencyCode } from '$lib/utils/currency'; import { convertToCHF, isValidCurrencyCode } from '$lib/utils/currency';
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import cache, { invalidateCospendCaches } from '$lib/server/cache';
interface SplitInput { interface SplitInput {
username: string; username: string;
@@ -25,14 +24,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
await dbConnect(); await dbConnect();
try { try {
// Try cache first (include pagination params in key)
const cacheKey = `cospend:payments:list:${limit}:${offset}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const payments = await Payment.find() const payments = await Payment.find()
.populate('splits') .populate('splits')
.sort({ date: -1, createdAt: -1 }) .sort({ date: -1, createdAt: -1 })
@@ -40,16 +31,9 @@ export const GET: RequestHandler = async ({ locals, url }) => {
.skip(offset) .skip(offset)
.lean(); .lean();
const result = { payments }; return json({ payments });
// Cache for 10 minutes (shorter TTL since this changes frequently)
await cache.set(cacheKey, JSON.stringify(result), 600);
return json(result);
} catch (e) { } catch (e) {
throw error(500, 'Failed to fetch payments'); throw error(500, 'Failed to fetch payments');
} finally {
// Connection will be reused
} }
}; };
@@ -89,7 +73,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const totalPersonal = splits.reduce((sum: number, split: SplitInput) => { const totalPersonal = splits.reduce((sum: number, split: SplitInput) => {
return sum + (split.personalAmount ?? 0); return sum + (split.personalAmount ?? 0);
}, 0); }, 0);
if (totalPersonal > amount) { if (totalPersonal > amount) {
throw error(400, 'Personal amounts cannot exceed total payment amount'); throw error(400, 'Personal amounts cannot exceed total payment amount');
} }
@@ -114,7 +98,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
} }
await dbConnect(); await dbConnect();
try { try {
const payment = await Payment.create({ const payment = await Payment.create({
title, title,
@@ -135,7 +119,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const convertedSplits = splits.map((split: SplitInput) => { const convertedSplits = splits.map((split: SplitInput) => {
let convertedAmount = split.amount; let convertedAmount = split.amount;
let convertedPersonalAmount = split.personalAmount; let convertedPersonalAmount = split.personalAmount;
// Convert amounts if we have a foreign currency // Convert amounts if we have a foreign currency
if (inputCurrency !== 'CHF' && exchangeRate) { if (inputCurrency !== 'CHF' && exchangeRate) {
convertedAmount = split.amount * exchangeRate; convertedAmount = split.amount * exchangeRate;
@@ -143,7 +127,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
convertedPersonalAmount = split.personalAmount * exchangeRate; convertedPersonalAmount = split.personalAmount * exchangeRate;
} }
} }
return { return {
paymentId: payment._id, paymentId: payment._id,
username: split.username, username: split.username,
@@ -153,16 +137,12 @@ export const POST: RequestHandler = async ({ request, locals }) => {
}; };
}); });
const splitPromises = convertedSplits.map((split: { paymentId: unknown; username: string; amount: number; proportion?: number; personalAmount?: number }) => { const splitPromises = convertedSplits.map((split) => {
return PaymentSplit.create(split); return PaymentSplit.create(split as any);
}); });
await Promise.all(splitPromises); await Promise.all(splitPromises);
// Invalidate caches for all affected users
const affectedUsernames = splits.map((split: SplitInput) => split.username);
await invalidateCospendCaches(affectedUsernames, payment._id.toString());
return json({ return json({
success: true, success: true,
payment: payment._id payment: payment._id
@@ -171,7 +151,5 @@ export const POST: RequestHandler = async ({ request, locals }) => {
} catch (e) { } catch (e) {
console.error('Error creating payment:', e); console.error('Error creating payment:', e);
throw error(500, 'Failed to create payment'); throw error(500, 'Failed to create payment');
} finally {
// Connection will be reused
} }
}; };
@@ -3,7 +3,6 @@ import { Payment } from '$models/Payment';
import { PaymentSplit } from '$models/PaymentSplit'; import { PaymentSplit } from '$models/PaymentSplit';
import { dbConnect } from '$utils/db'; import { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import cache, { invalidateCospendCaches } from '$lib/server/cache';
export const GET: RequestHandler = async ({ params, locals }) => { export const GET: RequestHandler = async ({ params, locals }) => {
const auth = await locals.auth(); const auth = await locals.auth();
@@ -16,31 +15,16 @@ export const GET: RequestHandler = async ({ params, locals }) => {
await dbConnect(); await dbConnect();
try { try {
// Try cache first
const cacheKey = `cospend:payment:${id}`;
const cached = await cache.get(cacheKey);
if (cached) {
return json(JSON.parse(cached));
}
const payment = await Payment.findById(id).populate('splits').lean(); const payment = await Payment.findById(id).populate('splits').lean();
if (!payment) { if (!payment) {
throw error(404, 'Payment not found'); throw error(404, 'Payment not found');
} }
const result = { payment }; return json({ payment });
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
} catch (e: unknown) { } catch (e: unknown) {
if (e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 404) throw e; if (e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 404) throw e;
throw error(500, 'Failed to fetch payment'); throw error(500, 'Failed to fetch payment');
} finally {
// Connection will be reused
} }
}; };
@@ -66,10 +50,6 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
throw error(403, 'Not authorized to edit this payment'); throw error(403, 'Not authorized to edit this payment');
} }
// Get old splits to invalidate caches for users who were in the original payment
const oldSplits = await PaymentSplit.find({ paymentId: id }).lean();
const oldUsernames = oldSplits.map(split => split.username);
const updatedPayment = await Payment.findByIdAndUpdate( const updatedPayment = await Payment.findByIdAndUpdate(
id, id,
{ {
@@ -82,12 +62,11 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
category: data.category || payment.category, category: data.category || payment.category,
splitMethod: data.splitMethod splitMethod: data.splitMethod
}, },
{ new: true } { returnDocument: 'after' }
); );
let newUsernames: string[] = [];
if (data.splits) { if (data.splits) {
await PaymentSplit.deleteMany({ paymentId: id }); await PaymentSplit.deleteMany({ paymentId: id } as any);
const splitPromises = data.splits.map((split: { username: string; amount: number; proportion?: number; personalAmount?: number }) => { const splitPromises = data.splits.map((split: { username: string; amount: number; proportion?: number; personalAmount?: number }) => {
return PaymentSplit.create({ return PaymentSplit.create({
@@ -96,23 +75,16 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
amount: split.amount, amount: split.amount,
proportion: split.proportion, proportion: split.proportion,
personalAmount: split.personalAmount personalAmount: split.personalAmount
}); } as any);
}); });
await Promise.all(splitPromises); await Promise.all(splitPromises);
newUsernames = data.splits.map((split: { username: string }) => split.username);
} }
// Invalidate caches for all users (old and new)
const allAffectedUsers = [...new Set([...oldUsernames, ...newUsernames])];
await invalidateCospendCaches(allAffectedUsers, id);
return json({ success: true, payment: updatedPayment }); return json({ success: true, payment: updatedPayment });
} catch (e: unknown) { } catch (e: unknown) {
if (e && typeof e === 'object' && 'status' in e) throw e; if (e && typeof e === 'object' && 'status' in e) throw e;
throw error(500, 'Failed to update payment'); throw error(500, 'Failed to update payment');
} finally {
// Connection will be reused
} }
}; };
@@ -137,21 +109,12 @@ export const DELETE: RequestHandler = async ({ params, locals }) => {
throw error(403, 'Not authorized to delete this payment'); throw error(403, 'Not authorized to delete this payment');
} }
// Get splits to invalidate caches for affected users await PaymentSplit.deleteMany({ paymentId: id } as any);
const splits = await PaymentSplit.find({ paymentId: id }).lean();
const affectedUsernames = splits.map(split => split.username);
await PaymentSplit.deleteMany({ paymentId: id });
await Payment.findByIdAndDelete(id); await Payment.findByIdAndDelete(id);
// Invalidate caches for all affected users
await invalidateCospendCaches(affectedUsernames, id);
return json({ success: true }); return json({ success: true });
} catch (e: unknown) { } catch (e: unknown) {
if (e && typeof e === 'object' && 'status' in e) throw e; if (e && typeof e === 'object' && 'status' in e) throw e;
throw error(500, 'Failed to delete payment'); throw error(500, 'Failed to delete payment');
} finally {
// Connection will be reused
} }
}; };
@@ -134,7 +134,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const recurringPayment = await RecurringPayment.findByIdAndUpdate( const recurringPayment = await RecurringPayment.findByIdAndUpdate(
id, id,
updateData, updateData,
{ new: true, runValidators: true } { returnDocument: 'after', runValidators: true }
); );
return json({ return json({
@@ -6,7 +6,6 @@ import { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit';
import { calculateNextExecutionDate } from '$lib/utils/recurring'; import { calculateNextExecutionDate } from '$lib/utils/recurring';
import { convertToCHF } from '$lib/utils/currency'; import { convertToCHF } from '$lib/utils/currency';
import { invalidateCospendCaches } from '$lib/server/cache';
export const POST: RequestHandler = async ({ locals }) => { export const POST: RequestHandler = async ({ locals }) => {
const auth = await locals.auth(); const auth = await locals.auth();
@@ -15,10 +14,10 @@ export const POST: RequestHandler = async ({ locals }) => {
} }
await dbConnect(); await dbConnect();
try { try {
const now = new Date(); const now = new Date();
// Find all active recurring payments that are due // Find all active recurring payments that are due
const duePayments = await RecurringPayment.find({ const duePayments = await RecurringPayment.find({
isActive: true, isActive: true,
@@ -42,8 +41,8 @@ export const POST: RequestHandler = async ({ locals }) => {
if (recurringPayment.currency !== 'CHF') { if (recurringPayment.currency !== 'CHF') {
try { try {
const conversion = await convertToCHF( const conversion = await convertToCHF(
recurringPayment.amount, recurringPayment.amount,
recurringPayment.currency, recurringPayment.currency,
now.toISOString() now.toISOString()
); );
finalAmount = conversion.convertedAmount; finalAmount = conversion.convertedAmount;
@@ -74,7 +73,7 @@ export const POST: RequestHandler = async ({ locals }) => {
const convertedSplits = recurringPayment.splits.map((split) => { const convertedSplits = recurringPayment.splits.map((split) => {
let convertedAmount = split.amount || 0; let convertedAmount = split.amount || 0;
let convertedPersonalAmount = split.personalAmount; let convertedPersonalAmount = split.personalAmount;
// Convert amounts if we have a foreign currency and exchange rate // Convert amounts if we have a foreign currency and exchange rate
if (recurringPayment.currency !== 'CHF' && exchangeRate && split.amount) { if (recurringPayment.currency !== 'CHF' && exchangeRate && split.amount) {
convertedAmount = split.amount * exchangeRate; convertedAmount = split.amount * exchangeRate;
@@ -82,7 +81,7 @@ export const POST: RequestHandler = async ({ locals }) => {
convertedPersonalAmount = split.personalAmount * exchangeRate; convertedPersonalAmount = split.personalAmount * exchangeRate;
} }
} }
return { return {
paymentId: payment._id, paymentId: payment._id,
username: split.username, username: split.username,
@@ -94,15 +93,11 @@ export const POST: RequestHandler = async ({ locals }) => {
// Create payment splits // Create payment splits
const splitPromises = convertedSplits.map((split) => { const splitPromises = convertedSplits.map((split) => {
return PaymentSplit.create(split); return PaymentSplit.create(split as any);
}); });
await Promise.all(splitPromises); await Promise.all(splitPromises);
// Invalidate caches for all affected users
const affectedUsernames = recurringPayment.splits.map((split) => split.username);
await invalidateCospendCaches(affectedUsernames, payment._id.toString());
// Calculate next execution date // Calculate next execution date
const nextExecutionDate = calculateNextExecutionDate(recurringPayment, now); const nextExecutionDate = calculateNextExecutionDate(recurringPayment, now);
@@ -133,7 +128,7 @@ export const POST: RequestHandler = async ({ locals }) => {
} }
} }
return json({ return json({
success: true, success: true,
executed: results.filter(r => r.success).length, executed: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length, failed: results.filter(r => !r.success).length,
@@ -143,7 +138,5 @@ export const POST: RequestHandler = async ({ locals }) => {
} catch (e) { } catch (e) {
console.error('Error executing recurring payments:', e); console.error('Error executing recurring payments:', e);
throw error(500, 'Failed to execute recurring payments'); throw error(500, 'Failed to execute recurring payments');
} finally {
// Connection will be reused
} }
}; };
@@ -28,7 +28,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await FavoriteIngredient.findOneAndUpdate( await FavoriteIngredient.findOneAndUpdate(
{ createdBy: user.nickname, source, sourceId: String(sourceId) }, { createdBy: user.nickname, source, sourceId: String(sourceId) },
{ createdBy: user.nickname, source, sourceId: String(sourceId), name }, { createdBy: user.nickname, source, sourceId: String(sourceId), name },
{ upsert: true, new: true } { upsert: true, returnDocument: 'after' }
); );
return json({ ok: true }, { status: 201 }); return json({ ok: true }, { status: 201 });
+1 -1
View File
@@ -56,7 +56,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
const goal = await FitnessGoal.findOneAndUpdate( const goal = await FitnessGoal.findOneAndUpdate(
{ username: user.nickname }, { username: user.nickname },
update, update,
{ upsert: true, new: true } { upsert: true, returnDocument: 'after' }
).lean() as any; ).lean() as any;
const streak = await computeStreak(user.nickname, weeklyWorkouts); const streak = await computeStreak(user.nickname, weeklyWorkouts);
@@ -60,7 +60,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const template = await IntervalTemplate.findOneAndUpdate( const template = await IntervalTemplate.findOneAndUpdate(
{ _id: params.id, createdBy: session.user.nickname }, { _id: params.id, createdBy: session.user.nickname },
{ name, steps }, { name, steps },
{ new: true } { returnDocument: 'after' }
); );
if (!template) { if (!template) {
@@ -47,7 +47,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const measurement = await BodyMeasurement.findOneAndUpdate( const measurement = await BodyMeasurement.findOneAndUpdate(
{ _id: params.id, createdBy: user.nickname }, { _id: params.id, createdBy: user.nickname },
updateData, updateData,
{ new: true } { returnDocument: 'after' }
); );
if (!measurement) { if (!measurement) {
+1 -1
View File
@@ -85,7 +85,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
const schedule = await WorkoutSchedule.findOneAndUpdate( const schedule = await WorkoutSchedule.findOneAndUpdate(
{ userId: user.nickname }, { userId: user.nickname },
{ templateOrder }, { templateOrder },
{ upsert: true, new: true } { upsert: true, returnDocument: 'after' }
); );
return json({ schedule: { templateOrder: schedule.templateOrder } }); return json({ schedule: { templateOrder: schedule.templateOrder } });
@@ -119,7 +119,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
createdBy: session.user.nickname createdBy: session.user.nickname
}, },
updateData, updateData,
{ new: true } { returnDocument: 'after' }
); );
if (!workoutSession) { if (!workoutSession) {
@@ -87,7 +87,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
exercises: exercises ?? [], exercises: exercises ?? [],
isPublic isPublic
}, },
{ new: true } { returnDocument: 'after' }
); );
if (!template) { if (!template) {
@@ -73,7 +73,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
}, },
$setOnInsert: { userId } $setOnInsert: { userId }
}, },
{ upsert: true, new: true, lean: true } { upsert: true, returnDocument: 'after', lean: true }
); );
// Broadcast to all other connected devices // Broadcast to all other connected devices
@@ -64,7 +64,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const updated = await AngelusStreak.findOneAndUpdate( const updated = await AngelusStreak.findOneAndUpdate(
{ username: session.user.nickname }, { username: session.user.nickname },
updateFields, updateFields,
{ upsert: true, new: true } { upsert: true, returnDocument: 'after' }
).lean() as any; ).lean() as any;
return json({ return json({
@@ -54,7 +54,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const updated = await RosaryStreak.findOneAndUpdate( const updated = await RosaryStreak.findOneAndUpdate(
{ username: session.user.nickname }, { username: session.user.nickname },
updateFields, updateFields,
{ upsert: true, new: true } { upsert: true, returnDocument: 'after' }
).lean() as any; ).lean() as any;
return json({ return json({
@@ -34,7 +34,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const overwrite = await NutritionOverwrite.findOneAndUpdate( const overwrite = await NutritionOverwrite.findOneAndUpdate(
{ ingredientNameDe: data.ingredientNameDe }, { ingredientNameDe: data.ingredientNameDe },
data, data,
{ upsert: true, new: true, runValidators: true }, { upsert: true, returnDocument: 'after', runValidators: true },
).lean(); ).lean();
invalidateOverwriteCache(); invalidateOverwriteCache();
+1 -1
View File
@@ -7,7 +7,7 @@
import PaymentModal from '$lib/components/cospend/PaymentModal.svelte'; import PaymentModal from '$lib/components/cospend/PaymentModal.svelte';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import UserHeader from '$lib/components/UserHeader.svelte'; import UserHeader from '$lib/components/UserHeader.svelte';
import { LayoutDashboard, Wallet, RefreshCw } from 'lucide-svelte'; import { LayoutDashboard, Wallet, RefreshCw } from '@lucide/svelte';
let { data, children } = $props(); let { data, children } = $props();
+1 -1
View File
@@ -4,7 +4,7 @@
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import UserHeader from '$lib/components/UserHeader.svelte'; import UserHeader from '$lib/components/UserHeader.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte'; import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import { BarChart3, Clock, Dumbbell, ListChecks, Ruler, UtensilsCrossed } from 'lucide-svelte'; import { BarChart3, Clock, Dumbbell, ListChecks, Ruler, UtensilsCrossed } from '@lucide/svelte';
import { getWorkout } from '$lib/js/workout.svelte'; import { getWorkout } from '$lib/js/workout.svelte';
import { getWorkoutSync } from '$lib/js/workoutSync.svelte'; import { getWorkoutSync } from '$lib/js/workoutSync.svelte';
import WorkoutFab from '$lib/components/fitness/WorkoutFab.svelte'; import WorkoutFab from '$lib/components/fitness/WorkoutFab.svelte';
@@ -1,7 +1,7 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Search } from 'lucide-svelte'; import { Search } from '@lucide/svelte';
import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises'; import { getFilterOptions, searchExercises, translateTerm } from '$lib/data/exercises';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
@@ -1,7 +1,7 @@
<script> <script>
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Clock, Weight, Trophy, Trash2, Pencil, Plus, Upload, Route, X, RefreshCw, Gauge, Flame, Info } from 'lucide-svelte'; import { Clock, Weight, Trophy, Trash2, Pencil, Plus, Upload, Route, X, RefreshCw, Gauge, Flame, Info } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte'; import { toast } from '$lib/js/toast.svelte';
@@ -1,6 +1,6 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Pencil, Trash2, ChevronDown } from 'lucide-svelte'; import { Pencil, Trash2, ChevronDown } from '@lucide/svelte';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte'; import { toast } from '$lib/js/toast.svelte';
@@ -3,7 +3,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte'; import { toast } from '$lib/js/toast.svelte';
import { Trash2 } from 'lucide-svelte'; import { Trash2 } from '@lucide/svelte';
import SaveFab from '$lib/components/SaveFab.svelte'; import SaveFab from '$lib/components/SaveFab.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname)); const lang = $derived(detectFitnessLang($page.url.pathname));
@@ -44,7 +44,7 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
try { try {
await dbConnect(); await dbConnect();
const recipes = await Recipe.find( const recipes = await Recipe.find(
{ _id: { $in: [...new Set(recipeIds)] } }, { _id: { $in: [...new Set(recipeIds)] } as any },
{ _id: 1, short_name: 1, 'images.mediapath': 1 } { _id: 1, short_name: 1, 'images.mediapath': 1 }
).lean(); ).lean();
for (const r of recipes as any[]) { for (const r of recipes as any[]) {
@@ -1,7 +1,7 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto, invalidateAll } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { ChevronLeft, ChevronRight, Plus, Trash2, ChevronDown, Settings, Coffee, Sun, Moon, Cookie, Utensils, Info, UtensilsCrossed } from 'lucide-svelte'; import { ChevronLeft, ChevronRight, Plus, Trash2, ChevronDown, Settings, Coffee, Sun, Moon, Cookie, Utensils, Info, UtensilsCrossed } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import AddButton from '$lib/components/AddButton.svelte'; import AddButton from '$lib/components/AddButton.svelte';
import FoodSearch from '$lib/components/fitness/FoodSearch.svelte'; import FoodSearch from '$lib/components/fitness/FoodSearch.svelte';
@@ -1,6 +1,6 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { ChevronLeft, ChevronDown } from 'lucide-svelte'; import { ChevronLeft, ChevronDown } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import { NUTRIENT_META } from '$lib/data/dailyReferenceIntake'; import { NUTRIENT_META } from '$lib/data/dailyReferenceIntake';
@@ -1,7 +1,7 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { ChevronLeft, Plus, Trash2, Pencil, UtensilsCrossed, X } from 'lucide-svelte'; import { ChevronLeft, Plus, Trash2, Pencil, UtensilsCrossed, X } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte'; import { toast } from '$lib/js/toast.svelte';
import FoodSearch from '$lib/components/fitness/FoodSearch.svelte'; import FoodSearch from '$lib/components/fitness/FoodSearch.svelte';
@@ -1,7 +1,7 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte'; import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
import { Dumbbell, Route, Flame, Weight } from 'lucide-svelte'; import { Dumbbell, Route, Flame, Weight } from '@lucide/svelte';
import FitnessStreakAura from '$lib/components/fitness/FitnessStreakAura.svelte'; import FitnessStreakAura from '$lib/components/fitness/FitnessStreakAura.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Plus, Trash2, Play, Pencil, X, Save, CalendarClock, ChevronUp, ChevronDown, ArrowRight, MapPin, Dumbbell, Timer } from 'lucide-svelte'; import { Plus, Trash2, Play, Pencil, X, Save, CalendarClock, ChevronUp, ChevronDown, ArrowRight, MapPin, Dumbbell, Timer } from '@lucide/svelte';
import { getWorkout } from '$lib/js/workout.svelte'; import { getWorkout } from '$lib/js/workout.svelte';
import { getWorkoutSync } from '$lib/js/workoutSync.svelte'; import { getWorkoutSync } from '$lib/js/workoutSync.svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
@@ -1,7 +1,7 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Trash2, Play, Pause, Trophy, Clock, Dumbbell, Route, RefreshCw, Check, ChevronUp, ChevronDown, Flame, MapPin, Volume2, X, Timer, Plus, GripVertical } from 'lucide-svelte'; import { Trash2, Play, Pause, Trophy, Clock, Dumbbell, Route, RefreshCw, Check, ChevronUp, ChevronDown, Flame, MapPin, Volume2, X, Timer, Plus, GripVertical } from '@lucide/svelte';
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname)); const lang = $derived(detectFitnessLang($page.url.pathname));
+1 -1
View File
@@ -2,7 +2,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import UserHeader from '$lib/components/UserHeader.svelte'; import UserHeader from '$lib/components/UserHeader.svelte';
import { ClipboardList, Trophy } from 'lucide-svelte'; import { ClipboardList, Trophy } from '@lucide/svelte';
let { data, children } = $props(); let { data, children } = $props();
let user = $derived(data.session?.user); let user = $derived(data.session?.user);
+1 -1
View File
@@ -4,7 +4,7 @@
import { de } from 'date-fns/locale'; import { de } from 'date-fns/locale';
import { Plus, Check, Pencil, Trash2, Tag, Users, RotateCcw, Calendar, import { Plus, Check, Pencil, Trash2, Tag, Users, RotateCcw, Calendar,
Sparkles, Wind, Bath, UtensilsCrossed, CookingPot, WashingMachine, Sparkles, Wind, Bath, UtensilsCrossed, CookingPot, WashingMachine,
Flower2, Droplets, Leaf, ShoppingCart, Shirt, Brush } from 'lucide-svelte'; Flower2, Droplets, Leaf, ShoppingCart, Shirt, Brush } from '@lucide/svelte';
import { fly, scale } from 'svelte/transition'; import { fly, scale } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import TaskForm from '$lib/components/tasks/TaskForm.svelte'; import TaskForm from '$lib/components/tasks/TaskForm.svelte';
+1 -1
View File
@@ -4,7 +4,7 @@
import { de } from 'date-fns/locale'; import { de } from 'date-fns/locale';
import { scale } from 'svelte/transition'; import { scale } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { Trash2 } from 'lucide-svelte'; import { Trash2 } from '@lucide/svelte';
import StickerCalendar from '$lib/components/tasks/StickerCalendar.svelte'; import StickerCalendar from '$lib/components/tasks/StickerCalendar.svelte';
let { data } = $props(); let { data } = $props();
+1 -8
View File
@@ -10,14 +10,7 @@ export default defineConfig({
exclude: ['barcode-detector'] exclude: ['barcode-detector']
}, },
build: { build: {
minify: 'terser', rolldownOptions: {
terserOptions: {
compress: {
drop_console: ['log', 'debug'],
drop_debugger: true
}
},
rollupOptions: {
output: { output: {
manualChunks: (id) => { manualChunks: (id) => {
// Separate large dependencies into their own chunks // Separate large dependencies into their own chunks