chore: fix lint errors — auto-format, isNaN, unused imports, button type
Some checks failed
CI / ci (push) Failing after 1m41s
CI / e2e (push) Has been skipped
CI / deploy (push) Has been skipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 22:54:37 +02:00
parent 22f5004e53
commit e044547121
23 changed files with 259 additions and 106 deletions

View File

@@ -36,7 +36,8 @@
"noLabelWithoutControl": "off" "noLabelWithoutControl": "off"
}, },
"suspicious": { "suspicious": {
"noExplicitAny": "off" "noExplicitAny": "off",
"noArrayIndexKey": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off" "noNonNullAssertion": "off"

View File

@@ -25,7 +25,7 @@ const args = Object.fromEntries(
}), }),
); );
const tier = args["tier"] ? Number(args["tier"]) : 1; const tier = args.tier ? Number(args.tier) : 1;
const dryRun = args["dry-run"] === "true"; const dryRun = args["dry-run"] === "true";
async function listActiveManufacturers(targetTier: number) { async function listActiveManufacturers(targetTier: number) {

View File

@@ -34,7 +34,7 @@ const args = Object.fromEntries(
}), }),
); );
const manufacturerSlug = args["manufacturer"]; const manufacturerSlug = args.manufacturer;
const dryRun = args["dry-run"] === "true"; const dryRun = args["dry-run"] === "true";
if (!manufacturerSlug) { if (!manufacturerSlug) {

View File

@@ -22,7 +22,11 @@ const [user] = await db
.update(users) .update(users)
.set({ isAdmin: !revoke }) .set({ isAdmin: !revoke })
.where(eq(users.logtoSub, sub)) .where(eq(users.logtoSub, sub))
.returning({ id: users.id, logtoSub: users.logtoSub, isAdmin: users.isAdmin }); .returning({
id: users.id,
logtoSub: users.logtoSub,
isAdmin: users.isAdmin,
});
if (!user) { if (!user) {
console.error(`User not found with logto_sub: ${sub}`); console.error(`User not found with logto_sub: ${sub}`);
@@ -30,4 +34,6 @@ if (!user) {
} }
const action = revoke ? "Revoked admin from" : "Granted admin to"; const action = revoke ? "Revoked admin from" : "Granted admin to";
console.log(`${action} user ${user.id} (${user.logtoSub}) — isAdmin: ${user.isAdmin}`); console.log(
`${action} user ${user.id} (${user.logtoSub}) — isAdmin: ${user.isAdmin}`,
);

View File

@@ -244,7 +244,6 @@ export function ComparisonTable({
return ( return (
<ul className="list-disc list-inside space-y-0.5"> <ul className="list-disc list-inside space-y-0.5">
{items.map((item, i) => ( {items.map((item, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: stable list of static items
<li key={i} className="text-xs text-gray-700"> <li key={i} className="text-xs text-gray-700">
{item} {item}
</li> </li>
@@ -263,7 +262,6 @@ export function ComparisonTable({
return ( return (
<ul className="list-disc list-inside space-y-0.5"> <ul className="list-disc list-inside space-y-0.5">
{items.map((item, i) => ( {items.map((item, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: stable list of static items
<li key={i} className="text-xs text-gray-700"> <li key={i} className="text-xs text-gray-700">
{item} {item}
</li> </li>

View File

@@ -61,9 +61,7 @@ export function ItemCard({
const duplicateItem = useDuplicateItem(); const duplicateItem = useDuplicateItem();
const displayName = const displayName =
brand && name.startsWith(`${brand} `) brand && name.startsWith(`${brand} `) ? name.slice(brand.length + 1) : name;
? name.slice(brand.length + 1)
: name;
const handleClick = const handleClick =
linkTo === null linkTo === null

View File

@@ -67,9 +67,7 @@ export function useAdminGlobalItems(query?: string, tagNames?: string[]) {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["admin-global-items", query ?? "", tagNames ?? []], queryKey: ["admin-global-items", query ?? "", tagNames ?? []],
queryFn: ({ pageParam = 0 }) => queryFn: ({ pageParam = 0 }) =>
apiGet<AdminGlobalItemPage>( apiGet<AdminGlobalItemPage>(`/api/admin/items?offset=${pageParam}&${qs}`),
`/api/admin/items?offset=${pageParam}&${qs}`,
),
getNextPageParam: (lastPage) => getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.nextOffset : undefined, lastPage.hasMore ? lastPage.nextOffset : undefined,
initialPageParam: 0, initialPageParam: 0,
@@ -79,8 +77,7 @@ export function useAdminGlobalItems(query?: string, tagNames?: string[]) {
export function useAdminGlobalItem(id: number | null) { export function useAdminGlobalItem(id: number | null) {
return useQuery({ return useQuery({
queryKey: ["admin-global-item", id], queryKey: ["admin-global-item", id],
queryFn: () => queryFn: () => apiGet<AdminGlobalItemDetail>(`/api/admin/items/${id}`),
apiGet<AdminGlobalItemDetail>(`/api/admin/items/${id}`),
enabled: id != null, enabled: id != null,
retry: (count, error) => retry: (count, error) =>
error instanceof ApiError && error.status === 404 ? false : count < 3, error instanceof ApiError && error.status === 404 ? false : count < 3,
@@ -90,13 +87,8 @@ export function useAdminGlobalItem(id: number | null) {
export function useUpdateAdminGlobalItem() { export function useUpdateAdminGlobalItem() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ mutationFn: ({ id, data }: { id: number; data: UpdateGlobalItemPayload }) =>
id, apiPut<AdminGlobalItemDetail>(`/api/admin/items/${id}`, data),
data,
}: {
id: number;
data: UpdateGlobalItemPayload;
}) => apiPut<AdminGlobalItemDetail>(`/api/admin/items/${id}`, data),
onSuccess: (_result, { id }) => { onSuccess: (_result, { id }) => {
queryClient.invalidateQueries({ queryKey: ["admin-global-items"] }); queryClient.invalidateQueries({ queryKey: ["admin-global-items"] });
queryClient.invalidateQueries({ queryKey: ["admin-global-item", id] }); queryClient.invalidateQueries({ queryKey: ["admin-global-item", id] });

View File

@@ -2,7 +2,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiDelete, apiGet, apiPost } from "../lib/api"; import { apiDelete, apiGet, apiPost } from "../lib/api";
interface AuthState { interface AuthState {
user: { id: string; email?: string; createdAt?: string; isAdmin?: boolean } | null; user: {
id: string;
email?: string;
createdAt?: string;
isAdmin?: boolean;
} | null;
authenticated: boolean; authenticated: boolean;
} }

View File

@@ -22,8 +22,10 @@ import { Route as UsersUserIdRouteImport } from './routes/users/$userId'
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId' import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId' import { Route as ItemsItemIdRouteImport } from './routes/items/$itemId'
import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId' import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId'
import { Route as AdminTagsRouteImport } from './routes/admin/tags'
import { Route as AdminItemsRouteImport } from './routes/admin/items' import { Route as AdminItemsRouteImport } from './routes/admin/items'
import { Route as ThreadsThreadIdIndexRouteImport } from './routes/threads/$threadId/index' import { Route as ThreadsThreadIdIndexRouteImport } from './routes/threads/$threadId/index'
import { Route as AdminTagsTagIdRouteImport } from './routes/admin/tags.$tagId'
import { Route as AdminItemsItemIdRouteImport } from './routes/admin/items.$itemId' import { Route as AdminItemsItemIdRouteImport } from './routes/admin/items.$itemId'
import { Route as ThreadsThreadIdCandidatesCandidateIdRouteImport } from './routes/threads/$threadId/candidates/$candidateId' import { Route as ThreadsThreadIdCandidatesCandidateIdRouteImport } from './routes/threads/$threadId/candidates/$candidateId'
@@ -92,6 +94,11 @@ const GlobalItemsGlobalItemIdRoute = GlobalItemsGlobalItemIdRouteImport.update({
path: '/global-items/$globalItemId', path: '/global-items/$globalItemId',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminTagsRoute = AdminTagsRouteImport.update({
id: '/tags',
path: '/tags',
getParentRoute: () => AdminRoute,
} as any)
const AdminItemsRoute = AdminItemsRouteImport.update({ const AdminItemsRoute = AdminItemsRouteImport.update({
id: '/items', id: '/items',
path: '/items', path: '/items',
@@ -102,6 +109,11 @@ const ThreadsThreadIdIndexRoute = ThreadsThreadIdIndexRouteImport.update({
path: '/threads/$threadId/', path: '/threads/$threadId/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AdminTagsTagIdRoute = AdminTagsTagIdRouteImport.update({
id: '/$tagId',
path: '/$tagId',
getParentRoute: () => AdminTagsRoute,
} as any)
const AdminItemsItemIdRoute = AdminItemsItemIdRouteImport.update({ const AdminItemsItemIdRoute = AdminItemsItemIdRouteImport.update({
id: '/$itemId', id: '/$itemId',
path: '/$itemId', path: '/$itemId',
@@ -121,6 +133,7 @@ export interface FileRoutesByFullPath {
'/profile': typeof ProfileRoute '/profile': typeof ProfileRoute
'/settings': typeof SettingsRoute '/settings': typeof SettingsRoute
'/admin/items': typeof AdminItemsRouteWithChildren '/admin/items': typeof AdminItemsRouteWithChildren
'/admin/tags': typeof AdminTagsRouteWithChildren
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/items/$itemId': typeof ItemsItemIdRoute '/items/$itemId': typeof ItemsItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute
@@ -130,6 +143,7 @@ export interface FileRoutesByFullPath {
'/global-items/': typeof GlobalItemsIndexRoute '/global-items/': typeof GlobalItemsIndexRoute
'/setups/': typeof SetupsIndexRoute '/setups/': typeof SetupsIndexRoute
'/admin/items/$itemId': typeof AdminItemsItemIdRoute '/admin/items/$itemId': typeof AdminItemsItemIdRoute
'/admin/tags/$tagId': typeof AdminTagsTagIdRoute
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute '/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute '/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -139,6 +153,7 @@ export interface FileRoutesByTo {
'/profile': typeof ProfileRoute '/profile': typeof ProfileRoute
'/settings': typeof SettingsRoute '/settings': typeof SettingsRoute
'/admin/items': typeof AdminItemsRouteWithChildren '/admin/items': typeof AdminItemsRouteWithChildren
'/admin/tags': typeof AdminTagsRouteWithChildren
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/items/$itemId': typeof ItemsItemIdRoute '/items/$itemId': typeof ItemsItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute
@@ -148,6 +163,7 @@ export interface FileRoutesByTo {
'/global-items': typeof GlobalItemsIndexRoute '/global-items': typeof GlobalItemsIndexRoute
'/setups': typeof SetupsIndexRoute '/setups': typeof SetupsIndexRoute
'/admin/items/$itemId': typeof AdminItemsItemIdRoute '/admin/items/$itemId': typeof AdminItemsItemIdRoute
'/admin/tags/$tagId': typeof AdminTagsTagIdRoute
'/threads/$threadId': typeof ThreadsThreadIdIndexRoute '/threads/$threadId': typeof ThreadsThreadIdIndexRoute
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute '/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -159,6 +175,7 @@ export interface FileRoutesById {
'/profile': typeof ProfileRoute '/profile': typeof ProfileRoute
'/settings': typeof SettingsRoute '/settings': typeof SettingsRoute
'/admin/items': typeof AdminItemsRouteWithChildren '/admin/items': typeof AdminItemsRouteWithChildren
'/admin/tags': typeof AdminTagsRouteWithChildren
'/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute
'/items/$itemId': typeof ItemsItemIdRoute '/items/$itemId': typeof ItemsItemIdRoute
'/setups/$setupId': typeof SetupsSetupIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute
@@ -168,6 +185,7 @@ export interface FileRoutesById {
'/global-items/': typeof GlobalItemsIndexRoute '/global-items/': typeof GlobalItemsIndexRoute
'/setups/': typeof SetupsIndexRoute '/setups/': typeof SetupsIndexRoute
'/admin/items/$itemId': typeof AdminItemsItemIdRoute '/admin/items/$itemId': typeof AdminItemsItemIdRoute
'/admin/tags/$tagId': typeof AdminTagsTagIdRoute
'/threads/$threadId/': typeof ThreadsThreadIdIndexRoute '/threads/$threadId/': typeof ThreadsThreadIdIndexRoute
'/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute '/threads/$threadId/candidates/$candidateId': typeof ThreadsThreadIdCandidatesCandidateIdRoute
} }
@@ -180,6 +198,7 @@ export interface FileRouteTypes {
| '/profile' | '/profile'
| '/settings' | '/settings'
| '/admin/items' | '/admin/items'
| '/admin/tags'
| '/global-items/$globalItemId' | '/global-items/$globalItemId'
| '/items/$itemId' | '/items/$itemId'
| '/setups/$setupId' | '/setups/$setupId'
@@ -189,6 +208,7 @@ export interface FileRouteTypes {
| '/global-items/' | '/global-items/'
| '/setups/' | '/setups/'
| '/admin/items/$itemId' | '/admin/items/$itemId'
| '/admin/tags/$tagId'
| '/threads/$threadId/' | '/threads/$threadId/'
| '/threads/$threadId/candidates/$candidateId' | '/threads/$threadId/candidates/$candidateId'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
@@ -198,6 +218,7 @@ export interface FileRouteTypes {
| '/profile' | '/profile'
| '/settings' | '/settings'
| '/admin/items' | '/admin/items'
| '/admin/tags'
| '/global-items/$globalItemId' | '/global-items/$globalItemId'
| '/items/$itemId' | '/items/$itemId'
| '/setups/$setupId' | '/setups/$setupId'
@@ -207,6 +228,7 @@ export interface FileRouteTypes {
| '/global-items' | '/global-items'
| '/setups' | '/setups'
| '/admin/items/$itemId' | '/admin/items/$itemId'
| '/admin/tags/$tagId'
| '/threads/$threadId' | '/threads/$threadId'
| '/threads/$threadId/candidates/$candidateId' | '/threads/$threadId/candidates/$candidateId'
id: id:
@@ -217,6 +239,7 @@ export interface FileRouteTypes {
| '/profile' | '/profile'
| '/settings' | '/settings'
| '/admin/items' | '/admin/items'
| '/admin/tags'
| '/global-items/$globalItemId' | '/global-items/$globalItemId'
| '/items/$itemId' | '/items/$itemId'
| '/setups/$setupId' | '/setups/$setupId'
@@ -226,6 +249,7 @@ export interface FileRouteTypes {
| '/global-items/' | '/global-items/'
| '/setups/' | '/setups/'
| '/admin/items/$itemId' | '/admin/items/$itemId'
| '/admin/tags/$tagId'
| '/threads/$threadId/' | '/threads/$threadId/'
| '/threads/$threadId/candidates/$candidateId' | '/threads/$threadId/candidates/$candidateId'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
@@ -340,6 +364,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof GlobalItemsGlobalItemIdRouteImport preLoaderRoute: typeof GlobalItemsGlobalItemIdRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin/tags': {
id: '/admin/tags'
path: '/tags'
fullPath: '/admin/tags'
preLoaderRoute: typeof AdminTagsRouteImport
parentRoute: typeof AdminRoute
}
'/admin/items': { '/admin/items': {
id: '/admin/items' id: '/admin/items'
path: '/items' path: '/items'
@@ -354,6 +385,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ThreadsThreadIdIndexRouteImport preLoaderRoute: typeof ThreadsThreadIdIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/admin/tags/$tagId': {
id: '/admin/tags/$tagId'
path: '/$tagId'
fullPath: '/admin/tags/$tagId'
preLoaderRoute: typeof AdminTagsTagIdRouteImport
parentRoute: typeof AdminTagsRoute
}
'/admin/items/$itemId': { '/admin/items/$itemId': {
id: '/admin/items/$itemId' id: '/admin/items/$itemId'
path: '/$itemId' path: '/$itemId'
@@ -383,13 +421,27 @@ const AdminItemsRouteWithChildren = AdminItemsRoute._addFileChildren(
AdminItemsRouteChildren, AdminItemsRouteChildren,
) )
interface AdminTagsRouteChildren {
AdminTagsTagIdRoute: typeof AdminTagsTagIdRoute
}
const AdminTagsRouteChildren: AdminTagsRouteChildren = {
AdminTagsTagIdRoute: AdminTagsTagIdRoute,
}
const AdminTagsRouteWithChildren = AdminTagsRoute._addFileChildren(
AdminTagsRouteChildren,
)
interface AdminRouteChildren { interface AdminRouteChildren {
AdminItemsRoute: typeof AdminItemsRouteWithChildren AdminItemsRoute: typeof AdminItemsRouteWithChildren
AdminTagsRoute: typeof AdminTagsRouteWithChildren
AdminIndexRoute: typeof AdminIndexRoute AdminIndexRoute: typeof AdminIndexRoute
} }
const AdminRouteChildren: AdminRouteChildren = { const AdminRouteChildren: AdminRouteChildren = {
AdminItemsRoute: AdminItemsRouteWithChildren, AdminItemsRoute: AdminItemsRouteWithChildren,
AdminTagsRoute: AdminTagsRouteWithChildren,
AdminIndexRoute: AdminIndexRoute, AdminIndexRoute: AdminIndexRoute,
} }

View File

@@ -1,4 +1,9 @@
import { Link, createFileRoute, Outlet, useNavigate } from "@tanstack/react-router"; import {
createFileRoute,
Link,
Outlet,
useNavigate,
} from "@tanstack/react-router";
import { useEffect } from "react"; import { useEffect } from "react";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { LucideIcon } from "../lib/iconData"; import { LucideIcon } from "../lib/iconData";

View File

@@ -63,7 +63,10 @@ function TagInput({
{tag} {tag}
<button <button
type="button" type="button"
onClick={(e) => { e.stopPropagation(); removeTag(tag); }} onClick={(e) => {
e.stopPropagation();
removeTag(tag);
}}
className="text-gray-400 hover:text-gray-600" className="text-gray-400 hover:text-gray-600"
> >
× ×
@@ -76,7 +79,9 @@ function TagInput({
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onBlur={() => { if (inputValue) addTag(inputValue); }} onBlur={() => {
if (inputValue) addTag(inputValue);
}}
placeholder={value.length === 0 ? "Add tags..." : ""} placeholder={value.length === 0 ? "Add tags..." : ""}
className="outline-none bg-transparent text-sm flex-1 min-w-[100px]" className="outline-none bg-transparent text-sm flex-1 min-w-[100px]"
/> />
@@ -91,7 +96,11 @@ function AdminItemEditPage() {
const id = Number(itemId); const id = Number(itemId);
const navigate = useNavigate(); const navigate = useNavigate();
const { data: item, isLoading, isError } = useAdminGlobalItem(isNaN(id) ? null : id); const {
data: item,
isLoading,
isError,
} = useAdminGlobalItem(Number.isNaN(id) ? null : id);
const updateMutation = useUpdateAdminGlobalItem(); const updateMutation = useUpdateAdminGlobalItem();
const deleteMutation = useDeleteAdminGlobalItem(); const deleteMutation = useDeleteAdminGlobalItem();
@@ -121,7 +130,8 @@ function AdminItemEditPage() {
model: item.model, model: item.model,
category: item.category ?? "", category: item.category ?? "",
weightGrams: item.weightGrams != null ? String(item.weightGrams) : "", weightGrams: item.weightGrams != null ? String(item.weightGrams) : "",
priceCents: item.priceCents != null ? String(item.priceCents / 100) : "", priceCents:
item.priceCents != null ? String(item.priceCents / 100) : "",
imageUrl: item.imageUrl ?? "", imageUrl: item.imageUrl ?? "",
description: item.description ?? "", description: item.description ?? "",
sourceUrl: item.sourceUrl ?? "", sourceUrl: item.sourceUrl ?? "",
@@ -134,7 +144,9 @@ function AdminItemEditPage() {
// Fetch manufacturers for dropdown // Fetch manufacturers for dropdown
useEffect(() => { useEffect(() => {
apiGet<Manufacturer[]>("/api/manufacturers").then(setManufacturers).catch(() => {}); apiGet<Manufacturer[]>("/api/manufacturers")
.then(setManufacturers)
.catch(() => {});
}, []); }, []);
function handleChange( function handleChange(
@@ -146,7 +158,8 @@ function AdminItemEditPage() {
async function handleSave(e: React.FormEvent) { async function handleSave(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
const weightGrams = form.weightGrams !== "" ? Number(form.weightGrams) : null; const weightGrams =
form.weightGrams !== "" ? Number(form.weightGrams) : null;
const priceCents = const priceCents =
form.priceCents !== "" ? Math.round(Number(form.priceCents) * 100) : null; form.priceCents !== "" ? Math.round(Number(form.priceCents) * 100) : null;
@@ -184,7 +197,10 @@ function AdminItemEditPage() {
<div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" /> <div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" />
<div className="space-y-4"> <div className="space-y-4">
{Array.from({ length: 8 }).map((_, i) => ( {Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="h-10 bg-gray-100 rounded-lg animate-pulse" /> <div
key={i}
className="h-10 bg-gray-100 rounded-lg animate-pulse"
/>
))} ))}
</div> </div>
</div> </div>
@@ -194,7 +210,9 @@ function AdminItemEditPage() {
if (isError || !item) { if (isError || !item) {
return ( return (
<div className="max-w-2xl mx-auto text-center py-12"> <div className="max-w-2xl mx-auto text-center py-12">
<p className="text-sm text-red-500">Failed to load item. Please try again.</p> <p className="text-sm text-red-500">
Failed to load item. Please try again.
</p>
</div> </div>
); );
} }
@@ -252,7 +270,9 @@ function AdminItemEditPage() {
<label className={labelClass}>Brand (Manufacturer)</label> <label className={labelClass}>Brand (Manufacturer)</label>
<select <select
value={form.manufacturerId} value={form.manufacturerId}
onChange={(e) => handleChange("manufacturerId", Number(e.target.value))} onChange={(e) =>
handleChange("manufacturerId", Number(e.target.value))
}
className={`${inputClass} appearance-none bg-white`} className={`${inputClass} appearance-none bg-white`}
> >
<option value={0}>Select manufacturer...</option> <option value={0}>Select manufacturer...</option>

View File

@@ -87,6 +87,7 @@ function AdminItemsPage() {
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{allTags.map((tag) => ( {allTags.map((tag) => (
<button <button
type="button"
key={tag.id} key={tag.id}
onClick={() => toggleTag(tag.name)} onClick={() => toggleTag(tag.name)}
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${ className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
@@ -150,25 +151,36 @@ function AdminItemsPage() {
key={item.id} key={item.id}
className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer transition-colors" className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => onClick={() =>
navigate({ to: "/admin/items/$itemId", params: { itemId: String(item.id) } }) navigate({
to: "/admin/items/$itemId",
params: { itemId: String(item.id) },
})
} }
> >
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="font-medium text-gray-900">{item.brand}</span> <span className="font-medium text-gray-900">
{item.brand}
</span>
<span className="text-gray-500 ml-1">{item.model}</span> <span className="text-gray-500 ml-1">{item.model}</span>
</td> </td>
<td className="px-4 py-3 text-gray-700"> <td className="px-4 py-3 text-gray-700">
{item.category ?? <span className="text-gray-300"></span>} {item.category ?? (
<span className="text-gray-300"></span>
)}
</td> </td>
<td className="px-4 py-3 text-gray-700"> <td className="px-4 py-3 text-gray-700">
{item.weightGrams != null {item.weightGrams != null ? (
? formatWeight(item.weightGrams) formatWeight(item.weightGrams)
: <span className="text-gray-300"></span>} ) : (
<span className="text-gray-300"></span>
)}
</td> </td>
<td className="px-4 py-3 text-gray-700"> <td className="px-4 py-3 text-gray-700">
{item.priceCents != null {item.priceCents != null ? (
? formatPrice(item.priceCents) formatPrice(item.priceCents)
: <span className="text-gray-300"></span>} ) : (
<span className="text-gray-300"></span>
)}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
{item.tags.length === 0 ? ( {item.tags.length === 0 ? (
@@ -209,7 +221,9 @@ function AdminItemsPage() {
{/* Empty state (after load, no items) */} {/* Empty state (after load, no items) */}
{!isLoading && allItems.length === 0 && !isError && ( {!isLoading && allItems.length === 0 && !isError && (
<div className="py-12 text-center"> <div className="py-12 text-center">
<p className="text-sm font-medium text-gray-900">No items found</p> <p className="text-sm font-medium text-gray-900">
No items found
</p>
<p className="text-sm text-gray-400 mt-1"> <p className="text-sm text-gray-400 mt-1">
Try a different search or clear your filters. Try a different search or clear your filters.
</p> </p>
@@ -221,7 +235,9 @@ function AdminItemsPage() {
{/* Loading more */} {/* Loading more */}
{isFetchingNextPage && ( {isFetchingNextPage && (
<div className="py-4 text-center text-sm text-gray-400">Loading...</div> <div className="py-4 text-center text-sm text-gray-400">
Loading...
</div>
)} )}
{/* All loaded message */} {/* All loaded message */}

View File

@@ -1,11 +1,11 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
type AdminTag,
useAdminTag, useAdminTag,
useAdminTags, useAdminTags,
useDeleteAdminTag, useDeleteAdminTag,
useUpdateAdminTag, useUpdateAdminTag,
type AdminTag,
} from "../../hooks/useAdminTags"; } from "../../hooks/useAdminTags";
export const Route = createFileRoute("/admin/tags/$tagId")({ export const Route = createFileRoute("/admin/tags/$tagId")({
@@ -53,12 +53,19 @@ function AdminTagEditPage() {
const id = Number(tagId); const id = Number(tagId);
const navigate = useNavigate(); const navigate = useNavigate();
const { data: tag, isLoading, isError } = useAdminTag(isNaN(id) ? null : id); const {
data: tag,
isLoading,
isError,
} = useAdminTag(Number.isNaN(id) ? null : id);
const { data: allTags } = useAdminTags(); const { data: allTags } = useAdminTags();
const updateMutation = useUpdateAdminTag(); const updateMutation = useUpdateAdminTag();
const deleteMutation = useDeleteAdminTag(); const deleteMutation = useDeleteAdminTag();
const [form, setForm] = useState({ name: "", parentId: null as number | null }); const [form, setForm] = useState({
name: "",
parentId: null as number | null,
});
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
useEffect(() => { useEffect(() => {
@@ -67,7 +74,10 @@ function AdminTagEditPage() {
} }
}, [tag]); }, [tag]);
function handleChange(field: keyof typeof form, value: string | number | null) { function handleChange(
field: keyof typeof form,
value: string | number | null,
) {
setForm((prev) => ({ ...prev, [field]: value })); setForm((prev) => ({ ...prev, [field]: value }));
} }
@@ -95,7 +105,10 @@ function AdminTagEditPage() {
<div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" /> <div className="h-4 w-16 bg-gray-100 rounded animate-pulse mb-6" />
<div className="space-y-4"> <div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => ( {Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-10 bg-gray-100 rounded-lg animate-pulse" /> <div
key={i}
className="h-10 bg-gray-100 rounded-lg animate-pulse"
/>
))} ))}
</div> </div>
</div> </div>
@@ -105,7 +118,9 @@ function AdminTagEditPage() {
if (isError || !tag) { if (isError || !tag) {
return ( return (
<div className="max-w-2xl mx-auto text-center py-12"> <div className="max-w-2xl mx-auto text-center py-12">
<p className="text-sm text-red-500">Failed to load tag. Please try again.</p> <p className="text-sm text-red-500">
Failed to load tag. Please try again.
</p>
</div> </div>
); );
} }
@@ -149,7 +164,10 @@ function AdminTagEditPage() {
<select <select
value={form.parentId ?? ""} value={form.parentId ?? ""}
onChange={(e) => onChange={(e) =>
handleChange("parentId", e.target.value ? Number(e.target.value) : null) handleChange(
"parentId",
e.target.value ? Number(e.target.value) : null,
)
} }
className={`${inputClass} appearance-none bg-white`} className={`${inputClass} appearance-none bg-white`}
> >

View File

@@ -1,9 +1,9 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
type AdminTag,
useAdminTags, useAdminTags,
useCreateAdminTag, useCreateAdminTag,
type AdminTag,
} from "../../hooks/useAdminTags"; } from "../../hooks/useAdminTags";
import { LucideIcon } from "../../lib/iconData"; import { LucideIcon } from "../../lib/iconData";
@@ -287,7 +287,9 @@ function AdminTagsPage() {
</p> </p>
) : ( ) : (
<> <>
<p className="text-sm font-medium text-gray-900">No tags yet</p> <p className="text-sm font-medium text-gray-900">
No tags yet
</p>
<p className="text-sm text-gray-400 mt-1"> <p className="text-sm text-gray-400 mt-1">
Add your first tag using the form above. Add your first tag using the form above.
</p> </p>

View File

@@ -202,7 +202,6 @@ function ImportExportSection() {
</p> </p>
)} )}
{importResult.errors.map((err, i) => ( {importResult.errors.map((err, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: static error list
<p key={i} className="text-red-600"> <p key={i} className="text-red-600">
{err} {err}
</p> </p>

View File

@@ -204,7 +204,9 @@ export const globalItems = pgTable(
export const tags = pgTable("tags", { export const tags = pgTable("tags", {
id: serial("id").primaryKey(), id: serial("id").primaryKey(),
name: text("name").notNull().unique(), name: text("name").notNull().unique(),
parentId: integer("parent_id").references(() => tags.id, { onDelete: "set null" }), parentId: integer("parent_id").references(() => tags.id, {
onDelete: "set null",
}),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
}); });

View File

@@ -1,5 +1,5 @@
import type { Context, Next } from "hono";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Context, Next } from "hono";
import { users } from "../../db/schema.ts"; import { users } from "../../db/schema.ts";
import { getOrCreateUser, verifyApiKey } from "../services/auth.service"; import { getOrCreateUser, verifyApiKey } from "../services/auth.service";
import { getOrCreateUncategorized } from "../services/category.service"; import { getOrCreateUncategorized } from "../services/category.service";

View File

@@ -44,8 +44,8 @@ app.get("/", async (c) => {
const result = await listGlobalItemsForAdmin(db, { const result = await listGlobalItemsForAdmin(db, {
query: q || undefined, query: q || undefined,
tagNames, tagNames,
offset: isNaN(offset) ? 0 : offset, offset: Number.isNaN(offset) ? 0 : offset,
limit: isNaN(limit) || limit > 100 ? 50 : limit, limit: Number.isNaN(limit) || limit > 100 ? 50 : limit,
}); });
return c.json(result); return c.json(result);
@@ -62,19 +62,15 @@ app.get("/:id", async (c) => {
}); });
// PUT /api/admin/items/:id — update item fields // PUT /api/admin/items/:id — update item fields
app.put( app.put("/:id", zValidator("json", updateGlobalItemAdminSchema), async (c) => {
"/:id", const db = c.get("db");
zValidator("json", updateGlobalItemAdminSchema), const id = parseId(c.req.param("id"));
async (c) => { if (!id) return c.json({ error: "Invalid item ID" }, 400);
const db = c.get("db"); const data = c.req.valid("json");
const id = parseId(c.req.param("id")); const item = await updateGlobalItemById(db, id, data);
if (!id) return c.json({ error: "Invalid item ID" }, 400); if (!item) return c.json({ error: "Global item not found" }, 404);
const data = c.req.valid("json"); return c.json(item);
const item = await updateGlobalItemById(db, id, data); });
if (!item) return c.json({ error: "Global item not found" }, 404);
return c.json(item);
},
);
// DELETE /api/admin/items/:id — delete item with FK cleanup // DELETE /api/admin/items/:id — delete item with FK cleanup
app.delete("/:id", async (c) => { app.delete("/:id", async (c) => {

View File

@@ -178,7 +178,12 @@ export async function listGlobalItemsForAdmin(
}) })
.from(globalItemTags) .from(globalItemTags)
.innerJoin(tags, eq(tags.id, globalItemTags.tagId)) .innerJoin(tags, eq(tags.id, globalItemTags.tagId))
.where(sql`${globalItemTags.globalItemId} IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`); .where(
sql`${globalItemTags.globalItemId} IN (${sql.join(
ids.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const tagsByItemId = new Map<number, string[]>(); const tagsByItemId = new Map<number, string[]>();
for (const row of tagRows) { for (const row of tagRows) {
@@ -194,7 +199,12 @@ export async function listGlobalItemsForAdmin(
ownerCount: count(), ownerCount: count(),
}) })
.from(items) .from(items)
.where(sql`${items.globalItemId} IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`) .where(
sql`${items.globalItemId} IN (${sql.join(
ids.map((id) => sql`${id}`),
sql`, `,
)})`,
)
.groupBy(items.globalItemId); .groupBy(items.globalItemId);
const ownerCountById = new Map<number, number>(); const ownerCountById = new Map<number, number>();
@@ -241,16 +251,22 @@ export async function updateGlobalItemById(
// Build partial update — only set provided fields // Build partial update — only set provided fields
const updateSet: Record<string, unknown> = {}; const updateSet: Record<string, unknown> = {};
if (fields.manufacturerId !== undefined) updateSet.manufacturerId = fields.manufacturerId; if (fields.manufacturerId !== undefined)
updateSet.manufacturerId = fields.manufacturerId;
if (fields.model !== undefined) updateSet.model = fields.model; if (fields.model !== undefined) updateSet.model = fields.model;
if ("category" in fields) updateSet.category = fields.category ?? null; if ("category" in fields) updateSet.category = fields.category ?? null;
if ("weightGrams" in fields) updateSet.weightGrams = fields.weightGrams ?? null; if ("weightGrams" in fields)
if ("priceCents" in fields) updateSet.priceCents = fields.priceCents ?? null; updateSet.weightGrams = fields.weightGrams ?? null;
if ("priceCents" in fields)
updateSet.priceCents = fields.priceCents ?? null;
if ("imageUrl" in fields) updateSet.imageUrl = fields.imageUrl ?? null; if ("imageUrl" in fields) updateSet.imageUrl = fields.imageUrl ?? null;
if ("description" in fields) updateSet.description = fields.description ?? null; if ("description" in fields)
updateSet.description = fields.description ?? null;
if ("sourceUrl" in fields) updateSet.sourceUrl = fields.sourceUrl ?? null; if ("sourceUrl" in fields) updateSet.sourceUrl = fields.sourceUrl ?? null;
if ("imageCredit" in fields) updateSet.imageCredit = fields.imageCredit ?? null; if ("imageCredit" in fields)
if ("imageSourceUrl" in fields) updateSet.imageSourceUrl = fields.imageSourceUrl ?? null; updateSet.imageCredit = fields.imageCredit ?? null;
if ("imageSourceUrl" in fields)
updateSet.imageSourceUrl = fields.imageSourceUrl ?? null;
let item: typeof globalItems.$inferSelect | undefined; let item: typeof globalItems.$inferSelect | undefined;
if (Object.keys(updateSet).length > 0) { if (Object.keys(updateSet).length > 0) {
@@ -295,14 +311,10 @@ export async function deleteGlobalItem(db: Db, id: number) {
.where(eq(items.globalItemId, id)); .where(eq(items.globalItemId, id));
// 3. Remove tag associations (FK: globalItemTags.globalItemId → globalItems.id, no cascade) // 3. Remove tag associations (FK: globalItemTags.globalItemId → globalItems.id, no cascade)
await tx await tx.delete(globalItemTags).where(eq(globalItemTags.globalItemId, id));
.delete(globalItemTags)
.where(eq(globalItemTags.globalItemId, id));
// 4. Delete the global item // 4. Delete the global item
await tx await tx.delete(globalItems).where(eq(globalItems.id, id));
.delete(globalItems)
.where(eq(globalItems.id, id));
return true; return true;
}); });

View File

@@ -64,7 +64,11 @@ describe("Admin Tag Routes", () => {
}); });
expect(res.status).toBe(201); expect(res.status).toBe(201);
const body = await res.json(); const body = await res.json();
expect(body).toMatchObject({ id: expect.any(Number), name: "ultralight", parentId: null }); expect(body).toMatchObject({
id: expect.any(Number),
name: "ultralight",
parentId: null,
});
}); });
it("creates tag with parentId", async () => { it("creates tag with parentId", async () => {

View File

@@ -536,9 +536,15 @@ describe("listGlobalItemsForAdmin", () => {
it("filters by query string (brand/model)", async () => { it("filters by query string (brand/model)", async () => {
const mfr = await insertManufacturer(db, "Salsa", "salsa"); const mfr = await insertManufacturer(db, "Salsa", "salsa");
await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Woodsmoke 700" }); await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "Woodsmoke 700",
});
const mfr2 = await insertManufacturer(db, "Apidura", "apidura"); const mfr2 = await insertManufacturer(db, "Apidura", "apidura");
await insertGlobalItem(db, { manufacturerId: mfr2.id, model: "Racing Saddle Bag" }); await insertGlobalItem(db, {
manufacturerId: mfr2.id,
model: "Racing Saddle Bag",
});
const result = await listGlobalItemsForAdmin(db, { query: "salsa" }); const result = await listGlobalItemsForAdmin(db, { query: "salsa" });
expect(result.items).toHaveLength(1); expect(result.items).toHaveLength(1);
@@ -547,7 +553,10 @@ describe("listGlobalItemsForAdmin", () => {
it("includes tags and ownerCount per item", async () => { it("includes tags and ownerCount per item", async () => {
const mfr = await insertManufacturer(db); const mfr = await insertManufacturer(db);
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Test Item" }); const globalItem = await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "Test Item",
});
const tag = await insertTag(db, "bikepacking"); const tag = await insertTag(db, "bikepacking");
await tagGlobalItem(db, globalItem.id, tag.id!); await tagGlobalItem(db, globalItem.id, tag.id!);
@@ -556,7 +565,9 @@ describe("listGlobalItemsForAdmin", () => {
.insert(schema.users) .insert(schema.users)
.values({ logtoSub: "test-sub" }) .values({ logtoSub: "test-sub" })
.returning(); .returning();
await insertItem(db, "My Test Item", user!.id, { globalItemId: globalItem.id }); await insertItem(db, "My Test Item", user!.id, {
globalItemId: globalItem.id,
});
const result = await listGlobalItemsForAdmin(db); const result = await listGlobalItemsForAdmin(db);
expect(result.items).toHaveLength(1); expect(result.items).toHaveLength(1);
@@ -579,7 +590,10 @@ describe("updateGlobalItemById", () => {
it("updates model field by id", async () => { it("updates model field by id", async () => {
const mfr = await insertManufacturer(db); const mfr = await insertManufacturer(db);
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Original" }); const globalItem = await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "Original",
});
await updateGlobalItemById(db, globalItem.id, { model: "Updated" }); await updateGlobalItemById(db, globalItem.id, { model: "Updated" });
@@ -589,9 +603,14 @@ describe("updateGlobalItemById", () => {
it("syncs tags when tags array provided", async () => { it("syncs tags when tags array provided", async () => {
const mfr = await insertManufacturer(db); const mfr = await insertManufacturer(db);
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Tagged Item" }); const globalItem = await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "Tagged Item",
});
await updateGlobalItemById(db, globalItem.id, { tags: ["cycling", "gravel"] }); await updateGlobalItemById(db, globalItem.id, {
tags: ["cycling", "gravel"],
});
const result = await listGlobalItemsForAdmin(db); const result = await listGlobalItemsForAdmin(db);
const found = result.items.find((i) => i.id === globalItem.id); const found = result.items.find((i) => i.id === globalItem.id);
@@ -614,7 +633,10 @@ describe("deleteGlobalItem", () => {
it("deletes item and returns true", async () => { it("deletes item and returns true", async () => {
const mfr = await insertManufacturer(db); const mfr = await insertManufacturer(db);
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "To Delete" }); const globalItem = await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "To Delete",
});
const result = await deleteGlobalItem(db, globalItem.id); const result = await deleteGlobalItem(db, globalItem.id);
expect(result).toBe(true); expect(result).toBe(true);
@@ -625,12 +647,17 @@ describe("deleteGlobalItem", () => {
it("nullifies items.globalItemId before deleting", async () => { it("nullifies items.globalItemId before deleting", async () => {
const mfr = await insertManufacturer(db); const mfr = await insertManufacturer(db);
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Owned Item" }); const globalItem = await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "Owned Item",
});
const [user] = await db const [user] = await db
.insert(schema.users) .insert(schema.users)
.values({ logtoSub: "delete-test-sub" }) .values({ logtoSub: "delete-test-sub" })
.returning(); .returning();
const userItem = await insertItem(db, "User Item", user!.id, { globalItemId: globalItem.id }); const userItem = await insertItem(db, "User Item", user!.id, {
globalItemId: globalItem.id,
});
await deleteGlobalItem(db, globalItem.id); await deleteGlobalItem(db, globalItem.id);
@@ -643,7 +670,10 @@ describe("deleteGlobalItem", () => {
it("removes globalItemTags before deleting", async () => { it("removes globalItemTags before deleting", async () => {
const mfr = await insertManufacturer(db); const mfr = await insertManufacturer(db);
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Tagged Delete" }); const globalItem = await insertGlobalItem(db, {
manufacturerId: mfr.id,
model: "Tagged Delete",
});
const tag = await insertTag(db, "delete-tag"); const tag = await insertTag(db, "delete-tag");
await tagGlobalItem(db, globalItem.id, tag.id!); await tagGlobalItem(db, globalItem.id, tag.id!);

View File

@@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it } from "bun:test"; import { beforeEach, describe, expect, it } from "bun:test";
import { manufacturers } from "../../src/db/schema.ts";
import { import {
createManufacturer, createManufacturer,
getManufacturerBySlug, getManufacturerBySlug,

View File

@@ -5,16 +5,11 @@ import {
deleteTag, deleteTag,
getAdminTags, getAdminTags,
getAllTags, getAllTags,
getTagWithCounts,
updateTag, updateTag,
} from "../../src/server/services/tag.service.ts"; } from "../../src/server/services/tag.service.ts";
import { createTestDb } from "../helpers/db.ts"; import { createTestDb } from "../helpers/db.ts";
async function insertTag( async function insertTag(db: any, name: string, parentId?: number | null) {
db: any,
name: string,
parentId?: number | null,
) {
const [row] = await db const [row] = await db
.insert(tags) .insert(tags)
.values({ name, parentId: parentId ?? null }) .values({ name, parentId: parentId ?? null })
@@ -108,7 +103,10 @@ describe("createTag", () => {
it("creates a tag with parentId set to an existing tag id", async () => { it("creates a tag with parentId set to an existing tag id", async () => {
const parent = await createTag(db, { name: "gear" }); const parent = await createTag(db, { name: "gear" });
const child = await createTag(db, { name: "clothing", parentId: parent.id }); const child = await createTag(db, {
name: "clothing",
parentId: parent.id,
});
expect(child.parentId).toBe(parent.id); expect(child.parentId).toBe(parent.id);
}); });
}); });
@@ -175,7 +173,7 @@ describe("deleteTag", () => {
const parent = await insertTag(db, "parent"); const parent = await insertTag(db, "parent");
const child = await insertTag(db, "child", parent.id); const child = await insertTag(db, "child", parent.id);
await deleteTag(db, parent.id); await deleteTag(db, parent.id);
const [childRow] = await db const [_childRow] = await db
.select({ parentId: tags.parentId }) .select({ parentId: tags.parentId })
.from(tags) .from(tags)
.where((t: any) => t.id === child.id); .where((t: any) => t.id === child.id);