ci: trigga på master + formatfix inför Gitea Actions
CI / Typecheck, test & build (push) Failing after 2s

This commit is contained in:
Sven (AAMOS AI)
2026-08-13 17:25:14 +07:00
parent 61d60931ad
commit c32a7e33c7
141 changed files with 40555 additions and 45210 deletions
+217 -61
View File
@@ -37,7 +37,10 @@ export async function buildErasurePlan(db: Database, userId: string): Promise<Er
const bankImages = await db
.select({ key: schema.aiTrainingBank.imageS3Key })
.from(schema.aiTrainingBank)
.innerJoin(schema.aiCorrections, eq(schema.aiTrainingBank.correctionId, schema.aiCorrections.id))
.innerJoin(
schema.aiCorrections,
eq(schema.aiTrainingBank.correctionId, schema.aiCorrections.id),
)
.where(eq(schema.aiCorrections.userId, userId));
for (const row of bankImages) {
if (row.key) imageReferences.push(row.key);
@@ -101,12 +104,7 @@ export async function buildErasurePlan(db: Database, userId: string): Promise<Er
const draftRecipeImages = await db
.select({ urls: schema.recipes.imageUrls })
.from(schema.recipes)
.where(
and(
eq(schema.recipes.creatorUserId, userId),
ne(schema.recipes.status, "published"),
),
);
.where(and(eq(schema.recipes.creatorUserId, userId), ne(schema.recipes.status, "published")));
for (const row of draftRecipeImages) {
for (const url of row.urls ?? []) {
if (url) imageReferences.push(url);
@@ -128,167 +126,261 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.delete(schema.userCredentials)
.where(eq(schema.userCredentials.userId, userId))
.returning({ userId: schema.userCredentials.userId });
if (credDel.length) log.push({ table: getTableName(schema.userCredentials), action: "delete", count: credDel.length });
if (credDel.length)
log.push({
table: getTableName(schema.userCredentials),
action: "delete",
count: credDel.length,
});
const adminTotpDel = await db
.delete(schema.adminTotp)
.where(eq(schema.adminTotp.userId, userId))
.returning({ userId: schema.adminTotp.userId });
if (adminTotpDel.length) log.push({ table: getTableName(schema.adminTotp), action: "delete", count: adminTotpDel.length });
if (adminTotpDel.length)
log.push({
table: getTableName(schema.adminTotp),
action: "delete",
count: adminTotpDel.length,
});
const emailTokensDel = await db
.delete(schema.emailVerificationTokens)
.where(eq(schema.emailVerificationTokens.userId, userId))
.returning({ id: schema.emailVerificationTokens.id });
if (emailTokensDel.length) log.push({ table: getTableName(schema.emailVerificationTokens), action: "delete", count: emailTokensDel.length });
if (emailTokensDel.length)
log.push({
table: getTableName(schema.emailVerificationTokens),
action: "delete",
count: emailTokensDel.length,
});
const pwTokensDel = await db
.delete(schema.passwordResetTokens)
.where(eq(schema.passwordResetTokens.userId, userId))
.returning({ id: schema.passwordResetTokens.id });
if (pwTokensDel.length) log.push({ table: getTableName(schema.passwordResetTokens), action: "delete", count: pwTokensDel.length });
if (pwTokensDel.length)
log.push({
table: getTableName(schema.passwordResetTokens),
action: "delete",
count: pwTokensDel.length,
});
const refreshDel = await db
.delete(schema.refreshTokens)
.where(eq(schema.refreshTokens.userId, userId))
.returning({ id: schema.refreshTokens.id });
if (refreshDel.length) log.push({ table: getTableName(schema.refreshTokens), action: "delete", count: refreshDel.length });
if (refreshDel.length)
log.push({
table: getTableName(schema.refreshTokens),
action: "delete",
count: refreshDel.length,
});
const consentsDel = await db
.delete(schema.userConsents)
.where(eq(schema.userConsents.userId, userId))
.returning({ userId: schema.userConsents.userId });
if (consentsDel.length) log.push({ table: getTableName(schema.userConsents), action: "delete", count: consentsDel.length });
if (consentsDel.length)
log.push({
table: getTableName(schema.userConsents),
action: "delete",
count: consentsDel.length,
});
const idemDel = await db
.delete(schema.idempotencyKeys)
.where(eq(schema.idempotencyKeys.userId, userId))
.returning({ key: schema.idempotencyKeys.key });
if (idemDel.length) log.push({ table: getTableName(schema.idempotencyKeys), action: "delete", count: idemDel.length });
if (idemDel.length)
log.push({
table: getTableName(schema.idempotencyKeys),
action: "delete",
count: idemDel.length,
});
const pushDel = await db
.delete(schema.pushTokens)
.where(eq(schema.pushTokens.userId, userId))
.returning({ token: schema.pushTokens.token });
if (pushDel.length) log.push({ table: getTableName(schema.pushTokens), action: "delete", count: pushDel.length });
if (pushDel.length)
log.push({ table: getTableName(schema.pushTokens), action: "delete", count: pushDel.length });
const notifDel = await db
.delete(schema.notifications)
.where(eq(schema.notifications.userId, userId))
.returning({ id: schema.notifications.id });
if (notifDel.length) log.push({ table: getTableName(schema.notifications), action: "delete", count: notifDel.length });
if (notifDel.length)
log.push({
table: getTableName(schema.notifications),
action: "delete",
count: notifDel.length,
});
const feedbackDel = await db
.delete(schema.feedback)
.where(eq(schema.feedback.userId, userId))
.returning({ id: schema.feedback.id });
if (feedbackDel.length) log.push({ table: getTableName(schema.feedback), action: "delete", count: feedbackDel.length });
if (feedbackDel.length)
log.push({ table: getTableName(schema.feedback), action: "delete", count: feedbackDel.length });
// ---- 2. Recipes ----
const deletedRecipes = await db
.delete(schema.recipes)
.where(
and(
eq(schema.recipes.creatorUserId, userId),
ne(schema.recipes.status, "published"),
),
)
.where(and(eq(schema.recipes.creatorUserId, userId), ne(schema.recipes.status, "published")))
.returning({ id: schema.recipes.id });
if (deletedRecipes.length) log.push({ table: getTableName(schema.recipes), action: "delete", count: deletedRecipes.length });
if (deletedRecipes.length)
log.push({
table: getTableName(schema.recipes),
action: "delete",
count: deletedRecipes.length,
});
const anonymizedRecipes = await db
.update(schema.recipes)
.set({ creatorUserId: null, creatorDisplayName: null, updatedAt: new Date() })
.where(
and(
eq(schema.recipes.creatorUserId, userId),
eq(schema.recipes.status, "published"),
),
)
.where(and(eq(schema.recipes.creatorUserId, userId), eq(schema.recipes.status, "published")))
.returning({ id: schema.recipes.id });
if (anonymizedRecipes.length) log.push({ table: getTableName(schema.recipes), action: "anonymize", count: anonymizedRecipes.length });
if (anonymizedRecipes.length)
log.push({
table: getTableName(schema.recipes),
action: "anonymize",
count: anonymizedRecipes.length,
});
const ratingsDel = await db
.delete(schema.recipeRatings)
.where(eq(schema.recipeRatings.userId, userId))
.returning({ id: schema.recipeRatings.id });
if (ratingsDel.length) log.push({ table: getTableName(schema.recipeRatings), action: "delete", count: ratingsDel.length });
if (ratingsDel.length)
log.push({
table: getTableName(schema.recipeRatings),
action: "delete",
count: ratingsDel.length,
});
const favoritesDel = await db
.delete(schema.recipeFavorites)
.where(eq(schema.recipeFavorites.userId, userId))
.returning({ recipeId: schema.recipeFavorites.recipeId });
if (favoritesDel.length) log.push({ table: getTableName(schema.recipeFavorites), action: "delete", count: favoritesDel.length });
if (favoritesDel.length)
log.push({
table: getTableName(schema.recipeFavorites),
action: "delete",
count: favoritesDel.length,
});
const cooksDel = await db
.delete(schema.recipeCooks)
.where(eq(schema.recipeCooks.userId, userId))
.returning({ id: schema.recipeCooks.id });
if (cooksDel.length) log.push({ table: getTableName(schema.recipeCooks), action: "delete", count: cooksDel.length });
if (cooksDel.length)
log.push({ table: getTableName(schema.recipeCooks), action: "delete", count: cooksDel.length });
const creatorStatsDel = await db
.delete(schema.creatorStats)
.where(eq(schema.creatorStats.userId, userId))
.returning({ userId: schema.creatorStats.userId });
if (creatorStatsDel.length) log.push({ table: getTableName(schema.creatorStats), action: "delete", count: creatorStatsDel.length });
if (creatorStatsDel.length)
log.push({
table: getTableName(schema.creatorStats),
action: "delete",
count: creatorStatsDel.length,
});
const followsDel = await db
.delete(schema.creatorFollows)
.where(
or(eq(schema.creatorFollows.followerUserId, userId), eq(schema.creatorFollows.creatorUserId, userId)),
or(
eq(schema.creatorFollows.followerUserId, userId),
eq(schema.creatorFollows.creatorUserId, userId),
),
)
.returning({ followerUserId: schema.creatorFollows.followerUserId });
if (followsDel.length) log.push({ table: getTableName(schema.creatorFollows), action: "delete", count: followsDel.length });
if (followsDel.length)
log.push({
table: getTableName(schema.creatorFollows),
action: "delete",
count: followsDel.length,
});
// ---- 3. Personal content ----
const mealsDel = await db
.delete(schema.meals)
.where(eq(schema.meals.userId, userId))
.returning({ id: schema.meals.id });
if (mealsDel.length) log.push({ table: getTableName(schema.meals), action: "delete", count: mealsDel.length });
if (mealsDel.length)
log.push({ table: getTableName(schema.meals), action: "delete", count: mealsDel.length });
const foodMemDel = await db
.delete(schema.foodMemories)
.where(eq(schema.foodMemories.userId, userId))
.returning({ id: schema.foodMemories.id });
if (foodMemDel.length) log.push({ table: getTableName(schema.foodMemories), action: "delete", count: foodMemDel.length });
if (foodMemDel.length)
log.push({
table: getTableName(schema.foodMemories),
action: "delete",
count: foodMemDel.length,
});
const tasteDel = await db
.delete(schema.tasteSignals)
.where(eq(schema.tasteSignals.userId, userId))
.returning({ id: schema.tasteSignals.id });
if (tasteDel.length) log.push({ table: getTableName(schema.tasteSignals), action: "delete", count: tasteDel.length });
if (tasteDel.length)
log.push({
table: getTableName(schema.tasteSignals),
action: "delete",
count: tasteDel.length,
});
const memoryDel = await db
.delete(schema.memoryItems)
.where(eq(schema.memoryItems.userId, userId))
.returning({ id: schema.memoryItems.id });
if (memoryDel.length) log.push({ table: getTableName(schema.memoryItems), action: "delete", count: memoryDel.length });
if (memoryDel.length)
log.push({
table: getTableName(schema.memoryItems),
action: "delete",
count: memoryDel.length,
});
// ---- 4. AI / scans / training ----
const aiCorrectionsDel = await db
.delete(schema.aiCorrections)
.where(eq(schema.aiCorrections.userId, userId))
.returning({ id: schema.aiCorrections.id });
if (aiCorrectionsDel.length) log.push({ table: getTableName(schema.aiCorrections), action: "delete", count: aiCorrectionsDel.length });
if (aiCorrectionsDel.length)
log.push({
table: getTableName(schema.aiCorrections),
action: "delete",
count: aiCorrectionsDel.length,
});
const scanJobsDel = await db
.delete(schema.scanJobs)
.where(eq(schema.scanJobs.userId, userId))
.returning({ id: schema.scanJobs.id });
if (scanJobsDel.length) log.push({ table: getTableName(schema.scanJobs), action: "delete", count: scanJobsDel.length });
if (scanJobsDel.length)
log.push({ table: getTableName(schema.scanJobs), action: "delete", count: scanJobsDel.length });
const aiUsageDel = await db
.delete(schema.aiUsageCounters)
.where(eq(schema.aiUsageCounters.userId, userId))
.returning({ userId: schema.aiUsageCounters.userId });
if (aiUsageDel.length) log.push({ table: getTableName(schema.aiUsageCounters), action: "delete", count: aiUsageDel.length });
if (aiUsageDel.length)
log.push({
table: getTableName(schema.aiUsageCounters),
action: "delete",
count: aiUsageDel.length,
});
const trialsDel = await db
.delete(schema.trials)
.where(eq(schema.trials.userId, userId))
.returning({ userId: schema.trials.userId });
if (trialsDel.length) log.push({ table: getTableName(schema.trials), action: "delete", count: trialsDel.length });
if (trialsDel.length)
log.push({ table: getTableName(schema.trials), action: "delete", count: trialsDel.length });
// ---- 5. Financial / subscriptions (pseudonymize) ----
const subCountRow = await db
@@ -299,7 +391,8 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.select({ count: sql<number>`count(*)::int` })
.from(schema.storeTransactions)
.where(eq(schema.storeTransactions.userId, userId));
const hasFinancialRecords = Number(subCountRow[0]?.count ?? 0) > 0 || Number(txCountRow[0]?.count ?? 0) > 0;
const hasFinancialRecords =
Number(subCountRow[0]?.count ?? 0) > 0 || Number(txCountRow[0]?.count ?? 0) > 0;
if (hasFinancialRecords) {
const pseudonym = randomUUID();
@@ -317,7 +410,12 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.set({ userId: null, pseudonym, householdId: null, updatedAt: new Date() })
.where(eq(schema.subscriptions.userId, userId))
.returning({ id: schema.subscriptions.id });
if (subPseud.length) log.push({ table: getTableName(schema.subscriptions), action: "pseudonymize", count: subPseud.length });
if (subPseud.length)
log.push({
table: getTableName(schema.subscriptions),
action: "pseudonymize",
count: subPseud.length,
});
const storePseud = await db
.update(schema.storeTransactions)
@@ -328,14 +426,24 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
})
.where(eq(schema.storeTransactions.userId, userId))
.returning({ id: schema.storeTransactions.id });
if (storePseud.length) log.push({ table: getTableName(schema.storeTransactions), action: "pseudonymize", count: storePseud.length });
if (storePseud.length)
log.push({
table: getTableName(schema.storeTransactions),
action: "pseudonymize",
count: storePseud.length,
});
const eventPseud = await db
.update(schema.subscriptionEvents)
.set({ userId: null, pseudonym, payload: sql`jsonb_build_object('anonymized', true)` })
.where(eq(schema.subscriptionEvents.userId, userId))
.returning({ id: schema.subscriptionEvents.id });
if (eventPseud.length) log.push({ table: getTableName(schema.subscriptionEvents), action: "pseudonymize", count: eventPseud.length });
if (eventPseud.length)
log.push({
table: getTableName(schema.subscriptionEvents),
action: "pseudonymize",
count: eventPseud.length,
});
}
// ---- 6. Audit logs (anonymize actor, keep for retention) ----
@@ -344,7 +452,12 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.set({ actorUserId: null, ip: null, metadata: sql`jsonb_build_object('anonymized', true)` })
.where(eq(schema.auditLogs.actorUserId, userId))
.returning({ id: schema.auditLogs.id });
if (auditAnon.length) log.push({ table: getTableName(schema.auditLogs), action: "anonymize", count: auditAnon.length });
if (auditAnon.length)
log.push({
table: getTableName(schema.auditLogs),
action: "anonymize",
count: auditAnon.length,
});
// ---- 7. Domain events (anonymize) ----
const domainAnon = await db
@@ -352,7 +465,12 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.set({ userId: null, payload: sql`jsonb_build_object('anonymized', true)` })
.where(eq(schema.domainEvents.userId, userId))
.returning({ id: schema.domainEvents.id });
if (domainAnon.length) log.push({ table: getTableName(schema.domainEvents), action: "anonymize", count: domainAnon.length });
if (domainAnon.length)
log.push({
table: getTableName(schema.domainEvents),
action: "anonymize",
count: domainAnon.length,
});
// ---- 8. Households ----
// Remove memberships first; remember surviving households for receipt anonymization.
@@ -366,7 +484,8 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.delete(schema.households)
.where(inArray(schema.households.id, plan.householdsToDelete))
.returning({ id: schema.households.id });
if (hhDel.length) log.push({ table: getTableName(schema.households), action: "delete", count: hhDel.length });
if (hhDel.length)
log.push({ table: getTableName(schema.households), action: "delete", count: hhDel.length });
}
const survivingHouseholdIds = survivingHouseholds
@@ -380,11 +499,20 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.where(inArray(schema.receipts.householdId, survivingHouseholdIds))
.returning({ id: schema.receipts.id });
if (receiptAnon.length) {
log.push({ table: getTableName(schema.receipts), action: "anonymize", count: receiptAnon.length });
log.push({
table: getTableName(schema.receipts),
action: "anonymize",
count: receiptAnon.length,
});
await db
.update(schema.receiptLines)
.set({ rawText: "" })
.where(inArray(schema.receiptLines.receiptId, receiptAnon.map((r) => r.id)));
.where(
inArray(
schema.receiptLines.receiptId,
receiptAnon.map((r) => r.id),
),
);
}
// Anonymize cooking sessions started by user in surviving households.
@@ -393,7 +521,12 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.set({ startedByUserId: null })
.where(eq(schema.cookingSessions.startedByUserId, userId))
.returning({ id: schema.cookingSessions.id });
if (cookingAnon.length) log.push({ table: getTableName(schema.cookingSessions), action: "anonymize", count: cookingAnon.length });
if (cookingAnon.length)
log.push({
table: getTableName(schema.cookingSessions),
action: "anonymize",
count: cookingAnon.length,
});
// Anonymize household-level records that still reference the user.
const conflictAnon = await db
@@ -401,38 +534,61 @@ export async function eraseUser(db: Database, userId: string): Promise<ErasureLo
.set({ resolvedByUserId: null })
.where(eq(schema.inventoryConflicts.resolvedByUserId, userId))
.returning({ id: schema.inventoryConflicts.id });
if (conflictAnon.length) log.push({ table: getTableName(schema.inventoryConflicts), action: "anonymize", count: conflictAnon.length });
if (conflictAnon.length)
log.push({
table: getTableName(schema.inventoryConflicts),
action: "anonymize",
count: conflictAnon.length,
});
const transactionAnon = await db
.update(schema.inventoryTransactions)
.set({ actorUserId: null })
.where(eq(schema.inventoryTransactions.actorUserId, userId))
.returning({ id: schema.inventoryTransactions.id });
if (transactionAnon.length) log.push({ table: getTableName(schema.inventoryTransactions), action: "anonymize", count: transactionAnon.length });
if (transactionAnon.length)
log.push({
table: getTableName(schema.inventoryTransactions),
action: "anonymize",
count: transactionAnon.length,
});
const mealBoxAnon = await db
.update(schema.mealBoxes)
.set({ reservedForUserId: null })
.where(eq(schema.mealBoxes.reservedForUserId, userId))
.returning({ id: schema.mealBoxes.id });
if (mealBoxAnon.length) log.push({ table: getTableName(schema.mealBoxes), action: "anonymize", count: mealBoxAnon.length });
if (mealBoxAnon.length)
log.push({
table: getTableName(schema.mealBoxes),
action: "anonymize",
count: mealBoxAnon.length,
});
const shoppingAnon = await db
.update(schema.shoppingListItems)
.set({ addedByUserId: null })
.where(eq(schema.shoppingListItems.addedByUserId, userId))
.returning({ id: schema.shoppingListItems.id });
if (shoppingAnon.length) log.push({ table: getTableName(schema.shoppingListItems), action: "anonymize", count: shoppingAnon.length });
if (shoppingAnon.length)
log.push({
table: getTableName(schema.shoppingListItems),
action: "anonymize",
count: shoppingAnon.length,
});
}
// ---- 9. Analytics ----
const analyticsDel = await deleteUserAnalyticsEvents(db, userId);
if (analyticsDel > 0) log.push({ table: "analytics_events", action: "delete", count: analyticsDel });
if (analyticsDel > 0)
log.push({ table: "analytics_events", action: "delete", count: analyticsDel });
// ---- 10. Health/preferences/locale (delete) ----
await db.delete(schema.userHealthProfiles).where(eq(schema.userHealthProfiles.userId, userId));
await db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, userId));
await db.delete(schema.userLocalePreferences).where(eq(schema.userLocalePreferences.userId, userId));
await db
.delete(schema.userLocalePreferences)
.where(eq(schema.userLocalePreferences.userId, userId));
return log;
}