feat: trash (soft delete) with admin tool management
Build & Push Docker Image / build (push) Successful in 2m15s
Build & Push Docker Image / build (push) Successful in 2m15s
- tools: add deletedAt/deletedBy, soft delete via DELETE /tools/:id when actor has trash entitlement, else immediate hard delete - trash endpoints: GET /tools/trash, POST /tools/trash (admin bulk), POST /tools/trash/restore, DELETE /tools/trash, POST /tools/trash/empty - trash feature for premium/enterprise; exclude trashed from all public surfaces (browse, categories, features, tags, similar, ratings, costs, analytics, redundancy) - TRASH_RETENTION_DAYS env (0 = keep forever) with hourly purge job - frontend: /trash page (premium+, restore for all, permanent delete + empty for admin), admin Tools tab with multi-select bulk trash, sidebar Trash link, tool-detail delete hint
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, sql, desc } from "drizzle-orm";
|
||||
import { eq, sql, desc, and } from "drizzle-orm";
|
||||
import { db, toolsTable, ratingsTable } from "@workspace/db";
|
||||
import {
|
||||
GetTopToolsQueryParams,
|
||||
@@ -11,7 +11,8 @@ const router: IRouter = Router();
|
||||
router.get("/analytics/summary", async (_req, res): Promise<void> => {
|
||||
const [toolStats] = await db
|
||||
.select({ totalTools: sql<number>`count(*)::int` })
|
||||
.from(toolsTable);
|
||||
.from(toolsTable)
|
||||
.where(sql`${toolsTable.deletedAt} IS NULL`);
|
||||
|
||||
const [ratingStats] = await db
|
||||
.select({
|
||||
@@ -23,7 +24,8 @@ router.get("/analytics/summary", async (_req, res): Promise<void> => {
|
||||
|
||||
const [catStats] = await db
|
||||
.select({ categoriesCount: sql<number>`count(distinct ${toolsTable.category})::int` })
|
||||
.from(toolsTable);
|
||||
.from(toolsTable)
|
||||
.where(sql`${toolsTable.deletedAt} IS NULL`);
|
||||
|
||||
const avgCombined = ratingStats.avgUsefulness != null && ratingStats.avgUsability != null
|
||||
? (Number(ratingStats.avgUsefulness) + Number(ratingStats.avgUsability)) / 2
|
||||
@@ -42,7 +44,7 @@ router.get("/analytics/summary", async (_req, res): Promise<void> => {
|
||||
|
||||
let mostRatedTool = null;
|
||||
if (mostRatedRow) {
|
||||
const [tool] = await db.select().from(toolsTable).where(eq(toolsTable.id, mostRatedRow.toolId));
|
||||
const [tool] = await db.select().from(toolsTable).where(and(eq(toolsTable.id, mostRatedRow.toolId), sql`${toolsTable.deletedAt} IS NULL`));
|
||||
if (tool) {
|
||||
const ratingRows = await db
|
||||
.select({ usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
|
||||
@@ -92,6 +94,7 @@ router.get("/analytics/top-tools", async (req, res): Promise<void> => {
|
||||
})
|
||||
.from(toolsTable)
|
||||
.innerJoin(ratingsTable, eq(ratingsTable.toolId, toolsTable.id))
|
||||
.where(sql`${toolsTable.deletedAt} IS NULL`)
|
||||
.groupBy(toolsTable.id)
|
||||
.orderBy(desc(scoreExpr))
|
||||
.limit(limit);
|
||||
@@ -121,6 +124,7 @@ router.get("/analytics/by-category", async (_req, res): Promise<void> => {
|
||||
})
|
||||
.from(toolsTable)
|
||||
.leftJoin(ratingsTable, eq(ratingsTable.toolId, toolsTable.id))
|
||||
.where(sql`${toolsTable.deletedAt} IS NULL`)
|
||||
.groupBy(toolsTable.category)
|
||||
.orderBy(toolsTable.category);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user