feat: major dependency upgrades, remove Redis, fix mongoose 9 types
CI / update (push) Successful in 4m10s

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 fbf888ab31
commit b2e271c3ea
68 changed files with 981 additions and 1743 deletions
@@ -2,7 +2,6 @@ import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { error } from '@sveltejs/kit';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request, cookies, locals}) => {
@@ -20,8 +19,6 @@ export const POST: RequestHandler = async ({request, cookies, locals}) => {
await dbConnect();
try{
await Recipe.create(recipe_json);
// Invalidate recipe caches after successful creation
await invalidateRecipeCaches();
} catch(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 type {RecipeModelType} from '$types/types';
import { error } from '@sveltejs/kit';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request, locals}) => {
@@ -70,9 +69,6 @@ export const POST: RequestHandler = async ({request, locals}) => {
// Delete the recipe
await Recipe.findOneAndDelete({short_name: short_name});
// Invalidate recipe caches after successful deletion
await invalidateRecipeCaches();
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
status: 200,
});
@@ -6,7 +6,6 @@ import { error } from '@sveltejs/kit';
import { rename } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
import { invalidateRecipeCaches } from '$lib/server/cache';
// header: use for bearer token for now
// 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);
// Invalidate recipe caches after successful update
await invalidateRecipeCaches();
return new Response(JSON.stringify({msg: "Edited recipe successfully"}),{
status: 200,
});
@@ -53,7 +53,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
await UserFavorites.findOneAndUpdate(
{ username: session.user.nickname },
{ $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!);
let recipes = await Recipe.find({
_id: { $in: userFavorites.favorites },
_id: { $in: userFavorites.favorites } as any,
...approvalFilter
}).lean() as unknown as RecipeModelType[];
@@ -1,26 +1,15 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '$types/types';
import { Recipe } from '$models/Recipe';
import { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
const cacheKey = `recipes:${params.recipeLang}:all_brief`;
let recipes: BriefRecipeType[] | null = null;
const cached = await cache.get(cacheKey);
await dbConnect();
const dbRecipes = await Recipe.find(approvalFilter, projection).lean();
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
if (cached) {
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!))));
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 { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, projection } = briefQueryConfig(params.recipeLang!);
const cacheKey = `recipes:${params.recipeLang}:in_season:${params.month}`;
let recipes = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
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);
}
await dbConnect();
const dbRecipes = await Recipe.find(
{ season: parseInt(params.month!, 10), icon: { $ne: "🍽️" }, ...approvalFilter },
projection
).lean();
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
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 { dbConnect } from '$utils/db';
import { rand_array } from '$lib/js/randomize';
import cache from '$lib/server/cache';
import { briefQueryConfig, toBrief } from '$lib/server/recipeHelpers';
export const GET: RequestHandler = async ({ params }) => {
const { approvalFilter, prefix, projection } = briefQueryConfig(params.recipeLang!);
const cacheKey = `recipes:${params.recipeLang}:tag:${params.tag}`;
let recipes = null;
const cached = await cache.get(cacheKey);
if (cached) {
recipes = JSON.parse(cached);
} else {
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);
}
await dbConnect();
const dbRecipes = await Recipe.find(
{ [`${prefix}tags`]: params.tag, ...approvalFilter },
projection
).lean();
const recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!));
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()),
notes: notes?.trim() || ''
},
{ new: true }
{ returnDocument: 'after' }
).lean();
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 { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit';
import cache from '$lib/server/cache';
export const GET: RequestHandler = async ({ locals, url }) => {
const auth = await locals.auth();
@@ -18,14 +17,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
try {
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([
{
$group: {
@@ -58,20 +49,9 @@ export const GET: RequestHandler = async ({ locals, url }) => {
allBalances: allSplits
};
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
} 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();
// Calculate net balance: negative = you are owed money, positive = you owe money
@@ -112,9 +92,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
recentSplits
};
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
}
@@ -122,4 +99,4 @@ export const GET: RequestHandler = async ({ locals, url }) => {
console.error('Error calculating balance:', e);
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 { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit';
import cache from '$lib/server/cache';
type PopulatedPayment = IPayment & { _id: import('mongoose').Types.ObjectId };
@@ -30,14 +29,6 @@ export const GET: RequestHandler = async ({ locals }) => {
await dbConnect();
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
const userSplits = await PaymentSplit.find({ username: currentUser })
.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
const paymentIds = userSplits.map(split => (split.paymentId as unknown as PopulatedPayment)._id);
const allRelatedSplits = await PaymentSplit.find({
paymentId: { $in: paymentIds },
paymentId: { $in: paymentIds } as any,
username: { $ne: currentUser }
})
.populate('paymentId')
@@ -115,15 +106,10 @@ export const GET: RequestHandler = async ({ locals }) => {
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);
} catch (e) {
console.error('Error calculating debt breakdown:', e);
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 { convertToCHF, isValidCurrencyCode } from '$lib/utils/currency';
import { error, json } from '@sveltejs/kit';
import cache, { invalidateCospendCaches } from '$lib/server/cache';
interface SplitInput {
username: string;
@@ -25,14 +24,6 @@ export const GET: RequestHandler = async ({ locals, url }) => {
await dbConnect();
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()
.populate('splits')
.sort({ date: -1, createdAt: -1 })
@@ -40,16 +31,9 @@ export const GET: RequestHandler = async ({ locals, url }) => {
.skip(offset)
.lean();
const result = { payments };
// Cache for 10 minutes (shorter TTL since this changes frequently)
await cache.set(cacheKey, JSON.stringify(result), 600);
return json(result);
return json({ payments });
} catch (e) {
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) => {
return sum + (split.personalAmount ?? 0);
}, 0);
if (totalPersonal > amount) {
throw error(400, 'Personal amounts cannot exceed total payment amount');
}
@@ -114,7 +98,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
}
await dbConnect();
try {
const payment = await Payment.create({
title,
@@ -135,7 +119,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const convertedSplits = splits.map((split: SplitInput) => {
let convertedAmount = split.amount;
let convertedPersonalAmount = split.personalAmount;
// Convert amounts if we have a foreign currency
if (inputCurrency !== 'CHF' && exchangeRate) {
convertedAmount = split.amount * exchangeRate;
@@ -143,7 +127,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
convertedPersonalAmount = split.personalAmount * exchangeRate;
}
}
return {
paymentId: payment._id,
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 }) => {
return PaymentSplit.create(split);
const splitPromises = convertedSplits.map((split) => {
return PaymentSplit.create(split as any);
});
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({
success: true,
payment: payment._id
@@ -171,7 +151,5 @@ export const POST: RequestHandler = async ({ request, locals }) => {
} catch (e) {
console.error('Error creating payment:', e);
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 { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit';
import cache, { invalidateCospendCaches } from '$lib/server/cache';
export const GET: RequestHandler = async ({ params, locals }) => {
const auth = await locals.auth();
@@ -16,31 +15,16 @@ export const GET: RequestHandler = async ({ params, locals }) => {
await dbConnect();
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();
if (!payment) {
throw error(404, 'Payment not found');
}
const result = { payment };
// Cache for 30 minutes
await cache.set(cacheKey, JSON.stringify(result), 1800);
return json(result);
return json({ payment });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 404) throw e;
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');
}
// 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(
id,
{
@@ -82,12 +62,11 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
category: data.category || payment.category,
splitMethod: data.splitMethod
},
{ new: true }
{ returnDocument: 'after' }
);
let newUsernames: string[] = [];
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 }) => {
return PaymentSplit.create({
@@ -96,23 +75,16 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
amount: split.amount,
proportion: split.proportion,
personalAmount: split.personalAmount
});
} as any);
});
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 });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'status' in e) throw e;
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');
}
// Get splits to invalidate caches for affected users
const splits = await PaymentSplit.find({ paymentId: id }).lean();
const affectedUsernames = splits.map(split => split.username);
await PaymentSplit.deleteMany({ paymentId: id });
await PaymentSplit.deleteMany({ paymentId: id } as any);
await Payment.findByIdAndDelete(id);
// Invalidate caches for all affected users
await invalidateCospendCaches(affectedUsernames, id);
return json({ success: true });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'status' in e) throw e;
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(
id,
updateData,
{ new: true, runValidators: true }
{ returnDocument: 'after', runValidators: true }
);
return json({
@@ -6,7 +6,6 @@ import { dbConnect } from '$utils/db';
import { error, json } from '@sveltejs/kit';
import { calculateNextExecutionDate } from '$lib/utils/recurring';
import { convertToCHF } from '$lib/utils/currency';
import { invalidateCospendCaches } from '$lib/server/cache';
export const POST: RequestHandler = async ({ locals }) => {
const auth = await locals.auth();
@@ -15,10 +14,10 @@ export const POST: RequestHandler = async ({ locals }) => {
}
await dbConnect();
try {
const now = new Date();
// Find all active recurring payments that are due
const duePayments = await RecurringPayment.find({
isActive: true,
@@ -42,8 +41,8 @@ export const POST: RequestHandler = async ({ locals }) => {
if (recurringPayment.currency !== 'CHF') {
try {
const conversion = await convertToCHF(
recurringPayment.amount,
recurringPayment.currency,
recurringPayment.amount,
recurringPayment.currency,
now.toISOString()
);
finalAmount = conversion.convertedAmount;
@@ -74,7 +73,7 @@ export const POST: RequestHandler = async ({ locals }) => {
const convertedSplits = recurringPayment.splits.map((split) => {
let convertedAmount = split.amount || 0;
let convertedPersonalAmount = split.personalAmount;
// Convert amounts if we have a foreign currency and exchange rate
if (recurringPayment.currency !== 'CHF' && exchangeRate && split.amount) {
convertedAmount = split.amount * exchangeRate;
@@ -82,7 +81,7 @@ export const POST: RequestHandler = async ({ locals }) => {
convertedPersonalAmount = split.personalAmount * exchangeRate;
}
}
return {
paymentId: payment._id,
username: split.username,
@@ -94,15 +93,11 @@ export const POST: RequestHandler = async ({ locals }) => {
// Create payment splits
const splitPromises = convertedSplits.map((split) => {
return PaymentSplit.create(split);
return PaymentSplit.create(split as any);
});
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
const nextExecutionDate = calculateNextExecutionDate(recurringPayment, now);
@@ -133,7 +128,7 @@ export const POST: RequestHandler = async ({ locals }) => {
}
}
return json({
return json({
success: true,
executed: 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) {
console.error('Error executing recurring payments:', e);
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(
{ createdBy: user.nickname, source, sourceId: String(sourceId) },
{ createdBy: user.nickname, source, sourceId: String(sourceId), name },
{ upsert: true, new: true }
{ upsert: true, returnDocument: 'after' }
);
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(
{ username: user.nickname },
update,
{ upsert: true, new: true }
{ upsert: true, returnDocument: 'after' }
).lean() as any;
const streak = await computeStreak(user.nickname, weeklyWorkouts);
@@ -60,7 +60,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const template = await IntervalTemplate.findOneAndUpdate(
{ _id: params.id, createdBy: session.user.nickname },
{ name, steps },
{ new: true }
{ returnDocument: 'after' }
);
if (!template) {
@@ -47,7 +47,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
const measurement = await BodyMeasurement.findOneAndUpdate(
{ _id: params.id, createdBy: user.nickname },
updateData,
{ new: true }
{ returnDocument: 'after' }
);
if (!measurement) {
+1 -1
View File
@@ -85,7 +85,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
const schedule = await WorkoutSchedule.findOneAndUpdate(
{ userId: user.nickname },
{ templateOrder },
{ upsert: true, new: true }
{ upsert: true, returnDocument: 'after' }
);
return json({ schedule: { templateOrder: schedule.templateOrder } });
@@ -119,7 +119,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
createdBy: session.user.nickname
},
updateData,
{ new: true }
{ returnDocument: 'after' }
);
if (!workoutSession) {
@@ -87,7 +87,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
exercises: exercises ?? [],
isPublic
},
{ new: true }
{ returnDocument: 'after' }
);
if (!template) {
@@ -73,7 +73,7 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
},
$setOnInsert: { userId }
},
{ upsert: true, new: true, lean: true }
{ upsert: true, returnDocument: 'after', lean: true }
);
// Broadcast to all other connected devices
@@ -64,7 +64,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const updated = await AngelusStreak.findOneAndUpdate(
{ username: session.user.nickname },
updateFields,
{ upsert: true, new: true }
{ upsert: true, returnDocument: 'after' }
).lean() as any;
return json({
@@ -54,7 +54,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const updated = await RosaryStreak.findOneAndUpdate(
{ username: session.user.nickname },
updateFields,
{ upsert: true, new: true }
{ upsert: true, returnDocument: 'after' }
).lean() as any;
return json({
@@ -34,7 +34,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const overwrite = await NutritionOverwrite.findOneAndUpdate(
{ ingredientNameDe: data.ingredientNameDe },
data,
{ upsert: true, new: true, runValidators: true },
{ upsert: true, returnDocument: 'after', runValidators: true },
).lean();
invalidateOverwriteCache();