feat: trash (soft delete) with admin tool management
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:
opencode
2026-08-02 02:02:16 +02:00
parent 32df998399
commit 2f20b1e5c2
25 changed files with 1404 additions and 28 deletions
+80 -1
View File
@@ -122,7 +122,9 @@ export const UpdateToolResponse = zod.object({
"features": zod.array(zod.string()).optional(),
"tags": zod.array(zod.string()).optional(),
"createdAt": zod.coerce.date(),
"updatedAt": zod.coerce.date()
"updatedAt": zod.coerce.date(),
"deletedAt": zod.coerce.date().nullish(),
"deletedBy": zod.string().nullish()
})
@@ -134,6 +136,83 @@ export const DeleteToolParams = zod.object({
})
/**
* @summary List trashed (soft-deleted) tools
*/
export const ListTrashedToolsQueryParams = zod.object({
"search": zod.coerce.string().optional()
})
export const ListTrashedToolsResponseItem = zod.object({
"id": zod.number(),
"name": zod.string(),
"description": zod.string(),
"category": zod.string(),
"websiteUrl": zod.string().nullish(),
"iconUrl": zod.string().nullish(),
"createdBy": zod.string().nullish(),
"features": zod.array(zod.string()).optional(),
"tags": zod.array(zod.string()).optional(),
"createdAt": zod.coerce.date(),
"updatedAt": zod.coerce.date(),
"deletedAt": zod.coerce.date().nullish(),
"deletedBy": zod.string().nullish()
})
export const ListTrashedToolsResponse = zod.array(ListTrashedToolsResponseItem)
/**
* @summary Move tools to trash (admin)
*/
export const trashToolsBodyIdsMax = 500;
export const TrashToolsBody = zod.object({
"ids": zod.array(zod.number()).min(1).max(trashToolsBodyIdsMax)
})
export const TrashToolsResponse = zod.object({
"trashed": zod.number().optional()
})
/**
* @summary Permanently delete trashed tools (admin)
*/
export const deleteTrashedToolsBodyIdsMax = 500;
export const DeleteTrashedToolsBody = zod.object({
"ids": zod.array(zod.number()).min(1).max(deleteTrashedToolsBodyIdsMax)
})
/**
* @summary Restore trashed tools
*/
export const restoreToolsBodyIdsMax = 500;
export const RestoreToolsBody = zod.object({
"ids": zod.array(zod.number()).min(1).max(restoreToolsBodyIdsMax)
})
export const RestoreToolsResponse = zod.object({
"restored": zod.number().optional()
})
/**
* @summary Permanently delete all trashed tools (admin)
*/
export const EmptyTrashResponse = zod.object({
"deleted": zod.number().optional()
})
/**
* @summary List ratings for a tool
*/
@@ -0,0 +1,11 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type EmptyTrash200 = {
deleted?: number;
};
+5
View File
@@ -14,6 +14,7 @@ export * from './authUser';
export * from './authUserRole';
export * from './authUserTier';
export * from './categoryStats';
export * from './emptyTrash200';
export * from './errorResponse';
export * from './getRatingDistributionParams';
export * from './getTopToolsMetric';
@@ -22,16 +23,20 @@ export * from './healthStatus';
export * from './listAuditLogsParams';
export * from './listToolsParams';
export * from './listToolsSort';
export * from './listTrashedToolsParams';
export * from './localLoginInput';
export * from './rating';
export * from './ratingDistribution';
export * from './ratingInput';
export * from './restoreTools200';
export * from './scoreBucket';
export * from './tool';
export * from './toolInput';
export * from './toolUpdate';
export * from './toolWithStats';
export * from './topToolEntry';
export * from './trashTools200';
export * from './trashToolsInput';
export * from './user';
export * from './userCreateInput';
export * from './userCreateInputRole';
@@ -0,0 +1,11 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type ListTrashedToolsParams = {
search?: string;
};
@@ -0,0 +1,11 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type RestoreTools200 = {
restored?: number;
};
+4
View File
@@ -21,4 +21,8 @@ export interface Tool {
tags?: string[];
createdAt: Date;
updatedAt: Date;
/** @nullable */
deletedAt?: Date | null;
/** @nullable */
deletedBy?: string | null;
}
@@ -0,0 +1,11 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type TrashTools200 = {
trashed?: number;
};
@@ -0,0 +1,15 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export interface TrashToolsInput {
/**
* @minItems 1
* @maxItems 500
*/
ids: number[];
}