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:
@@ -130,6 +130,10 @@ export interface Tool {
|
||||
tags?: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
/** @nullable */
|
||||
deletedAt?: string | null;
|
||||
/** @nullable */
|
||||
deletedBy?: string | null;
|
||||
}
|
||||
|
||||
export interface ToolWithStats {
|
||||
@@ -182,6 +186,14 @@ export interface ToolUpdate {
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface TrashToolsInput {
|
||||
/**
|
||||
* @minItems 1
|
||||
* @maxItems 500
|
||||
*/
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
export interface Rating {
|
||||
id: number;
|
||||
toolId: number;
|
||||
@@ -306,6 +318,22 @@ export const ListToolsSort = {
|
||||
most_reviewed: 'most_reviewed',
|
||||
} as const;
|
||||
|
||||
export type ListTrashedToolsParams = {
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TrashTools200 = {
|
||||
trashed?: number;
|
||||
};
|
||||
|
||||
export type RestoreTools200 = {
|
||||
restored?: number;
|
||||
};
|
||||
|
||||
export type EmptyTrash200 = {
|
||||
deleted?: number;
|
||||
};
|
||||
|
||||
export type GetTopToolsParams = {
|
||||
limit?: number;
|
||||
metric?: GetTopToolsMetric;
|
||||
|
||||
@@ -25,21 +25,26 @@ import type {
|
||||
AuthMode,
|
||||
AuthUser,
|
||||
CategoryStats,
|
||||
EmptyTrash200,
|
||||
ErrorResponse,
|
||||
GetRatingDistributionParams,
|
||||
GetTopToolsParams,
|
||||
HealthStatus,
|
||||
ListAuditLogsParams,
|
||||
ListToolsParams,
|
||||
ListTrashedToolsParams,
|
||||
LocalLoginInput,
|
||||
Rating,
|
||||
RatingDistribution,
|
||||
RatingInput,
|
||||
RestoreTools200,
|
||||
Tool,
|
||||
ToolInput,
|
||||
ToolUpdate,
|
||||
ToolWithStats,
|
||||
TopToolEntry,
|
||||
TrashTools200,
|
||||
TrashToolsInput,
|
||||
User,
|
||||
UserCreateInput,
|
||||
UserRoleUpdate
|
||||
@@ -509,6 +514,373 @@ export const useDeleteTool = <TError = ErrorType<ErrorResponse>,
|
||||
return useMutation(getDeleteToolMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getListTrashedToolsUrl = (params?: ListTrashedToolsParams,) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0 ? `/api/tools/trash?${stringifiedParams}` : `/api/tools/trash`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List trashed (soft-deleted) tools
|
||||
*/
|
||||
export const listTrashedTools = async (params?: ListTrashedToolsParams, options?: RequestInit): Promise<Tool[]> => {
|
||||
|
||||
return customFetch<Tool[]>(getListTrashedToolsUrl(params),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getListTrashedToolsQueryKey = (params?: ListTrashedToolsParams,) => {
|
||||
return [
|
||||
`/api/tools/trash`, ...(params ? [params] : [])
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getListTrashedToolsQueryOptions = <TData = Awaited<ReturnType<typeof listTrashedTools>>, TError = ErrorType<ErrorResponse>>(params?: ListTrashedToolsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listTrashedTools>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, request: requestOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getListTrashedToolsQueryKey(params);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listTrashedTools>>> = ({ signal }) => listTrashedTools(params, { signal, ...requestOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listTrashedTools>>, TError, TData> & { queryKey: QueryKey }
|
||||
}
|
||||
|
||||
export type ListTrashedToolsQueryResult = NonNullable<Awaited<ReturnType<typeof listTrashedTools>>>
|
||||
export type ListTrashedToolsQueryError = ErrorType<ErrorResponse>
|
||||
|
||||
|
||||
/**
|
||||
* @summary List trashed (soft-deleted) tools
|
||||
*/
|
||||
|
||||
export function useListTrashedTools<TData = Awaited<ReturnType<typeof listTrashedTools>>, TError = ErrorType<ErrorResponse>>(
|
||||
params?: ListTrashedToolsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listTrashedTools>>, TError, TData>, request?: SecondParameter<typeof customFetch>}
|
||||
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
|
||||
const queryOptions = getListTrashedToolsQueryOptions(params,options)
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & { queryKey: QueryKey };
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const getTrashToolsUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/tools/trash`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Move tools to trash (admin)
|
||||
*/
|
||||
export const trashTools = async (trashToolsInput: TrashToolsInput, options?: RequestInit): Promise<TrashTools200> => {
|
||||
|
||||
return customFetch<TrashTools200>(getTrashToolsUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
trashToolsInput,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getTrashToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
||||
|
||||
const mutationKey = ['trashTools'];
|
||||
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, request: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof trashTools>>, {data: BodyType<TrashToolsInput>}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return trashTools(data,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type TrashToolsMutationResult = NonNullable<Awaited<ReturnType<typeof trashTools>>>
|
||||
export type TrashToolsMutationBody = BodyType<TrashToolsInput>
|
||||
export type TrashToolsMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Move tools to trash (admin)
|
||||
*/
|
||||
export const useTrashTools = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof trashTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof trashTools>>,
|
||||
TError,
|
||||
{data: BodyType<TrashToolsInput>},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getTrashToolsMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getDeleteTrashedToolsUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/tools/trash`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Permanently delete trashed tools (admin)
|
||||
*/
|
||||
export const deleteTrashedTools = async (trashToolsInput: TrashToolsInput, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getDeleteTrashedToolsUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
trashToolsInput,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getDeleteTrashedToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
||||
|
||||
const mutationKey = ['deleteTrashedTools'];
|
||||
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, request: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof deleteTrashedTools>>, {data: BodyType<TrashToolsInput>}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return deleteTrashedTools(data,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type DeleteTrashedToolsMutationResult = NonNullable<Awaited<ReturnType<typeof deleteTrashedTools>>>
|
||||
export type DeleteTrashedToolsMutationBody = BodyType<TrashToolsInput>
|
||||
export type DeleteTrashedToolsMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Permanently delete trashed tools (admin)
|
||||
*/
|
||||
export const useDeleteTrashedTools = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteTrashedTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteTrashedTools>>,
|
||||
TError,
|
||||
{data: BodyType<TrashToolsInput>},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getDeleteTrashedToolsMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getRestoreToolsUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/tools/trash/restore`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Restore trashed tools
|
||||
*/
|
||||
export const restoreTools = async (trashToolsInput: TrashToolsInput, options?: RequestInit): Promise<RestoreTools200> => {
|
||||
|
||||
return customFetch<RestoreTools200>(getRestoreToolsUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
trashToolsInput,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getRestoreToolsMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext> => {
|
||||
|
||||
const mutationKey = ['restoreTools'];
|
||||
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, request: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof restoreTools>>, {data: BodyType<TrashToolsInput>}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return restoreTools(data,requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type RestoreToolsMutationResult = NonNullable<Awaited<ReturnType<typeof restoreTools>>>
|
||||
export type RestoreToolsMutationBody = BodyType<TrashToolsInput>
|
||||
export type RestoreToolsMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Restore trashed tools
|
||||
*/
|
||||
export const useRestoreTools = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof restoreTools>>, TError,{data: BodyType<TrashToolsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof restoreTools>>,
|
||||
TError,
|
||||
{data: BodyType<TrashToolsInput>},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getRestoreToolsMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getEmptyTrashUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `/api/tools/trash/empty`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Permanently delete all trashed tools (admin)
|
||||
*/
|
||||
export const emptyTrash = async ( options?: RequestInit): Promise<EmptyTrash200> => {
|
||||
|
||||
return customFetch<EmptyTrash200>(getEmptyTrashUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getEmptyTrashMutationOptions = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext> => {
|
||||
|
||||
const mutationKey = ['emptyTrash'];
|
||||
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, request: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof emptyTrash>>, void> = () => {
|
||||
|
||||
|
||||
return emptyTrash(requestOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type EmptyTrashMutationResult = NonNullable<Awaited<ReturnType<typeof emptyTrash>>>
|
||||
|
||||
export type EmptyTrashMutationError = ErrorType<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Permanently delete all trashed tools (admin)
|
||||
*/
|
||||
export const useEmptyTrash = <TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof emptyTrash>>, TError,void, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof emptyTrash>>,
|
||||
TError,
|
||||
void,
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getEmptyTrashMutationOptions(options));
|
||||
}
|
||||
|
||||
export const getListToolRatingsUrl = (id: number,) => {
|
||||
|
||||
|
||||
|
||||
@@ -165,6 +165,128 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/tools/trash:
|
||||
get:
|
||||
operationId: listTrashedTools
|
||||
tags: [tools]
|
||||
summary: List trashed (soft-deleted) tools
|
||||
parameters:
|
||||
- name: search
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: List of trashed tools
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Tool"
|
||||
"403":
|
||||
description: Feature "trash" requires a higher tier
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
post:
|
||||
operationId: trashTools
|
||||
tags: [tools]
|
||||
summary: Move tools to trash (admin)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TrashToolsInput"
|
||||
responses:
|
||||
"200":
|
||||
description: Tools trashed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
trashed:
|
||||
type: integer
|
||||
"403":
|
||||
description: Admin required
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
delete:
|
||||
operationId: deleteTrashedTools
|
||||
tags: [tools]
|
||||
summary: Permanently delete trashed tools (admin)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TrashToolsInput"
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted
|
||||
"403":
|
||||
description: Admin required
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/tools/trash/restore:
|
||||
post:
|
||||
operationId: restoreTools
|
||||
tags: [tools]
|
||||
summary: Restore trashed tools
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TrashToolsInput"
|
||||
responses:
|
||||
"200":
|
||||
description: Tools restored
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
restored:
|
||||
type: integer
|
||||
"403":
|
||||
description: Feature "trash" requires a higher tier
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/tools/trash/empty:
|
||||
post:
|
||||
operationId: emptyTrash
|
||||
tags: [tools]
|
||||
summary: Permanently delete all trashed tools (admin)
|
||||
responses:
|
||||
"200":
|
||||
description: Trash emptied
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
deleted:
|
||||
type: integer
|
||||
"403":
|
||||
description: Admin required
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/tools/{id}/ratings:
|
||||
get:
|
||||
operationId: listToolRatings
|
||||
@@ -658,6 +780,11 @@ components:
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
deletedAt:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
deletedBy:
|
||||
type: ["string", "null"]
|
||||
|
||||
ToolWithStats:
|
||||
type: object
|
||||
@@ -749,6 +876,17 @@ components:
|
||||
items:
|
||||
type: string
|
||||
|
||||
TrashToolsInput:
|
||||
type: object
|
||||
required: [ids]
|
||||
properties:
|
||||
ids:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 500
|
||||
items:
|
||||
type: integer
|
||||
|
||||
Rating:
|
||||
type: object
|
||||
required: [id, toolId, usefulness, usability, createdAt]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export const toolsTable = pgTable("tools", {
|
||||
tags: text("tags").array().notNull().default([]),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()),
|
||||
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
||||
deletedBy: text("deleted_by"),
|
||||
});
|
||||
|
||||
export const insertToolSchema = createInsertSchema(toolsTable).omit({ id: true, createdAt: true, updatedAt: true });
|
||||
|
||||
Reference in New Issue
Block a user