refactor/better-suspend-model #3
@@ -1,14 +1,15 @@
|
|||||||
import { ArticleList } from '@/ui/components/internal/article/article-list';
|
import { ArticleList } from '@/ui/components/internal/article/article-list';
|
||||||
import { ArticleListSkeleton } from '@/ui/components/internal/article/article-list-skeleton';
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
|
|
||||||
const PAGE_SIZE = 4;
|
const DEFAULT_PAGE_SIZE = 4;
|
||||||
|
|
||||||
type HomeProps = {
|
type HomeProps = {
|
||||||
searchParams: Promise<{ page?: string; pageSize?: string }>;
|
searchParams?: { page?: string; pageSize?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const Home = async ({ searchParams }: HomeProps) => {
|
const Home = async ({ searchParams }: HomeProps) => {
|
||||||
|
const page = Number(searchParams?.page) || 0;
|
||||||
|
const pageSize = Number(searchParams?.pageSize) || DEFAULT_PAGE_SIZE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container mx-auto w-full flex-1 px-4 py-12 md:py-16'>
|
<div className='container mx-auto w-full flex-1 px-4 py-12 md:py-16'>
|
||||||
<div className='mb-10 border-b border-border pb-8'>
|
<div className='mb-10 border-b border-border pb-8'>
|
||||||
@@ -19,14 +20,7 @@ const Home = async ({ searchParams }: HomeProps) => {
|
|||||||
Latest Articles
|
Latest Articles
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<Suspense
|
<ArticleList page={page} pageSize={pageSize} />
|
||||||
fallback={<ArticleListSkeleton skeletonSize={PAGE_SIZE} />}
|
|
||||||
>
|
|
||||||
<ArticleList
|
|
||||||
searchParams={searchParams}
|
|
||||||
defaultPageSize={PAGE_SIZE}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,43 +8,56 @@ import {
|
|||||||
} from '@/lib/feature/article/article.model';
|
} from '@/lib/feature/article/article.model';
|
||||||
import * as service from '@/lib/feature/article/article.service';
|
import * as service from '@/lib/feature/article/article.service';
|
||||||
import { getSessionData } from '@/lib/session/session-storage';
|
import { getSessionData } from '@/lib/session/session-storage';
|
||||||
import { TypedResult, wrap } from '@/utils/types/results';
|
import { TypedResult, wrap, wrapCached } from '@/utils/types/results';
|
||||||
import { UUIDv4 } from '@/utils/types/uuid';
|
import { UUIDv4 } from '@/utils/types/uuid';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidateTag } from 'next/cache';
|
||||||
|
|
||||||
export const getArticleByExternalId: (
|
export const getArticleByExternalId: (
|
||||||
externalId: UUIDv4
|
externalId: UUIDv4
|
||||||
) => Promise<TypedResult<ArticleModel | null>> = wrap(
|
) => Promise<TypedResult<ArticleModel | null>> = wrapCached(
|
||||||
async (externalId: UUIDv4): Promise<ArticleModel | null> => {
|
async (externalId: UUIDv4): Promise<ArticleModel | null> => {
|
||||||
const result = await service.getArticleByExternalId(externalId);
|
const result = await service.getArticleByExternalId(externalId);
|
||||||
if (!result.ok) throw result.error;
|
if (!result.ok) throw result.error;
|
||||||
return result.value;
|
return result.value;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: (id) => [`article:${id}`],
|
||||||
|
tags: (id) => ['articles', `article:${id}`],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getArticleBySlug: (
|
export const getArticleBySlug: (
|
||||||
slug: string
|
slug: string
|
||||||
) => Promise<TypedResult<ArticleModel | null>> = wrap(
|
) => Promise<TypedResult<ArticleModel | null>> = wrapCached(
|
||||||
async (slug: string): Promise<ArticleModel | null> => {
|
async (slug: string): Promise<ArticleModel | null> => {
|
||||||
const result = await service.getArticleBySlug(slug);
|
const result = await service.getArticleBySlug(slug);
|
||||||
if (!result.ok) throw result.error;
|
if (!result.ok) throw result.error;
|
||||||
return result.value;
|
return result.value;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: (slug) => [`article:slug:${slug}`],
|
||||||
|
tags: (slug) => ['articles', `article:slug:${slug}`],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getArticlesPaginated: (
|
export const getArticlesPaginated: (
|
||||||
page?: number,
|
page?: number,
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
) => Promise<TypedResult<PaginatedArticlesResult>> = wrap(
|
) => Promise<TypedResult<PaginatedArticlesResult>> = wrapCached(
|
||||||
async (
|
async (
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
pageSize: number = 10
|
pageSize: number = 10
|
||||||
): Promise<PaginatedArticlesResult> => {
|
): Promise<PaginatedArticlesResult> => {
|
||||||
// await new Promise((r) => setTimeout(r, 1000));
|
|
||||||
|
|
||||||
const result = await service.getArticlesPaginated(page, pageSize);
|
const result = await service.getArticlesPaginated(page, pageSize);
|
||||||
if (!result.ok) throw result.error;
|
if (!result.ok) throw result.error;
|
||||||
return result.value;
|
return result.value;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: (page, pageSize) => [`articles:page:${page}:${pageSize}`],
|
||||||
|
tags: (page, pageSize) => [
|
||||||
|
'articles',
|
||||||
|
`articles:page:${page}-${pageSize}`,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,6 +75,8 @@ export const saveArticle: (
|
|||||||
|
|
||||||
const result = await service.saveArticle(article);
|
const result = await service.saveArticle(article);
|
||||||
if (!result.ok) throw result.error;
|
if (!result.ok) throw result.error;
|
||||||
|
|
||||||
|
revalidateTag('articles', 'max');
|
||||||
return result.value;
|
return result.value;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -83,13 +98,19 @@ export const updateArticle: (
|
|||||||
|
|
||||||
const result = await service.updateArticle(articleId, article);
|
const result = await service.updateArticle(articleId, article);
|
||||||
if (!result.ok) throw result.error;
|
if (!result.ok) throw result.error;
|
||||||
revalidatePath('/admin');
|
|
||||||
|
revalidateTag('articles', 'max');
|
||||||
|
revalidateTag(`article:${articleId}`, 'max');
|
||||||
|
revalidateTag(`article:slug:${result.value.slug}`, 'max');
|
||||||
|
|
||||||
return result.value;
|
return result.value;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteArticle: (articleId: string) => Promise<TypedResult<void>> =
|
export const deleteArticleByExternalId: (
|
||||||
wrap(async (articleId: string): Promise<void> => {
|
externalId: UUIDv4
|
||||||
|
) => Promise<TypedResult<void>> = wrap(
|
||||||
|
async (externalId: UUIDv4): Promise<void> => {
|
||||||
const session = await getSessionData();
|
const session = await getSessionData();
|
||||||
if (!session || !session?.user || session?.user.role !== 'admin') {
|
if (!session || !session?.user || session?.user.role !== 'admin') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -97,7 +118,17 @@ export const deleteArticle: (articleId: string) => Promise<TypedResult<void>> =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await service.deleteArticle(articleId);
|
const getResult = await service.getArticleByExternalId(externalId);
|
||||||
|
if (!getResult.ok) throw getResult.error;
|
||||||
|
const article = getResult.value;
|
||||||
|
|
||||||
|
if (!article) throw new Error('Article not found');
|
||||||
|
|
||||||
|
const result = await service.deleteArticleByExternalId(externalId);
|
||||||
if (!result.ok) throw result.error;
|
if (!result.ok) throw result.error;
|
||||||
revalidatePath('/admin');
|
|
||||||
});
|
revalidateTag('articles', 'max');
|
||||||
|
revalidateTag(`article:${externalId}`, 'max');
|
||||||
|
revalidateTag(`article:slug:${article.slug}`, 'max');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -155,16 +155,19 @@ export const updateArticle: (
|
|||||||
);
|
);
|
||||||
|
|
||||||
/** Deletes an article from the database. */
|
/** Deletes an article from the database. */
|
||||||
export const deleteArticle: (articleId: string) => Promise<TypedResult<void>> =
|
export const deleteArticleByExternalId: (
|
||||||
wrap(async (articleId: string): Promise<void> => {
|
externalId: UUIDv4
|
||||||
|
) => Promise<TypedResult<void>> = wrap(
|
||||||
|
async (externalId: UUIDv4): Promise<void> => {
|
||||||
const articleRepository = await getRepository(ArticleEntity);
|
const articleRepository = await getRepository(ArticleEntity);
|
||||||
|
|
||||||
const existingArticle = await articleRepository.findOneBy({
|
const existingArticle = await articleRepository.findOneBy({
|
||||||
id: articleId,
|
externalId: externalId,
|
||||||
});
|
});
|
||||||
if (!existingArticle) {
|
if (!existingArticle) {
|
||||||
throw new Error(`Article with ID ${articleId} not found`);
|
throw new Error(`Article with ExternalID ${externalId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await articleRepository.remove(existingArticle);
|
await articleRepository.remove(existingArticle);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,30 +1,14 @@
|
|||||||
import { StorageProvider } from '@/lib/storage/storage.interface';
|
import { StorageProvider } from '@/lib/storage/storage.interface';
|
||||||
import { TypedResult, wrap } from '@/utils/types/results';
|
import { TypedResult, wrap } from '@/utils/types/results';
|
||||||
import { DeleteObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
import {
|
||||||
|
DeleteObjectCommand,
|
||||||
|
HeadObjectCommand,
|
||||||
|
PutObjectCommand,
|
||||||
|
S3Client,
|
||||||
|
} from '@aws-sdk/client-s3';
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for S3 storage adapter
|
* Configuration for S3 storage adapter
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export const AdminArticleCard = ({ article }: AdminArticleCardProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<DeleteArticleButton
|
<DeleteArticleButton
|
||||||
articleId={article.id}
|
externalID={article.externalId}
|
||||||
articleTitle={article.title}
|
title={article.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ export const AdminArticleList = async ({
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<DeleteArticleButton
|
<DeleteArticleButton
|
||||||
articleId={article.id}
|
externalID={article.externalId}
|
||||||
articleTitle={article.title}
|
title={article.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
// 'use client';
|
|
||||||
import { getArticlesPaginated } from '@/lib/feature/article/article.external';
|
import { getArticlesPaginated } from '@/lib/feature/article/article.external';
|
||||||
import { ArticleCard } from '@/ui/components/internal/article-card';
|
import { ArticleCard } from '@/ui/components/internal/article-card';
|
||||||
import { ArticleListPagination } from '@/ui/components/internal/article-list-pagination';
|
import { ArticleListPagination } from '@/ui/components/internal/article-list-pagination';
|
||||||
import { FileTextIcon } from 'lucide-react';
|
import { FileTextIcon } from 'lucide-react';
|
||||||
|
import { cacheTag } from 'next/cache';
|
||||||
|
|
||||||
type ArticleListProps = {
|
type ArticleListProps = {
|
||||||
searchParams: Promise<{ page?: string; pageSize?: string }>;
|
page: number;
|
||||||
defaultPageSize: number;
|
pageSize: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArticleList = async ({
|
export const ArticleList = async ({ page, pageSize }: ArticleListProps) => {
|
||||||
searchParams,
|
'use cache';
|
||||||
defaultPageSize,
|
|
||||||
}: ArticleListProps) => {
|
|
||||||
const { page: pageParam, pageSize: pageSizeParam } = await searchParams;
|
|
||||||
const page = Math.max(1, Number(pageParam) || 1);
|
|
||||||
const pageSize = Number(pageSizeParam) || defaultPageSize;
|
|
||||||
|
|
||||||
|
cacheTag('articles', `articles:page:${page}-${pageSize}`);
|
||||||
const paginationResult = await getArticlesPaginated(page, pageSize);
|
const paginationResult = await getArticlesPaginated(page, pageSize);
|
||||||
|
|
||||||
if (!paginationResult.ok) {
|
if (!paginationResult.ok) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { deleteArticle } from '@/lib/feature/article/article.external';
|
import { deleteArticleByExternalId } from '@/lib/feature/article/article.external';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -13,24 +13,25 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/ui/components/shadcn/alert-dialog';
|
} from '@/ui/components/shadcn/alert-dialog';
|
||||||
import { Button } from '@/ui/components/shadcn/button';
|
import { Button } from '@/ui/components/shadcn/button';
|
||||||
|
import { UUIDv4 } from '@/utils/types/uuid';
|
||||||
import { Trash2Icon } from 'lucide-react';
|
import { Trash2Icon } from 'lucide-react';
|
||||||
import { useTransition } from 'react';
|
import { useTransition } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface DeleteArticleButtonProps {
|
interface DeleteArticleButtonProps {
|
||||||
articleId: string;
|
externalID: UUIDv4;
|
||||||
articleTitle: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteArticleButton = ({
|
export const DeleteArticleButton = ({
|
||||||
articleId,
|
externalID,
|
||||||
articleTitle,
|
title,
|
||||||
}: DeleteArticleButtonProps) => {
|
}: DeleteArticleButtonProps) => {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const result = await deleteArticle(articleId);
|
const result = await deleteArticleByExternalId(externalID);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
toast.error('Failed to delete article', {
|
toast.error('Failed to delete article', {
|
||||||
description: result.error.message,
|
description: result.error.message,
|
||||||
@@ -39,7 +40,7 @@ export const DeleteArticleButton = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success('Article deleted', {
|
toast.success('Article deleted', {
|
||||||
description: `"${articleTitle}" has been removed.`,
|
description: `"${title}" has been removed.`,
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -57,7 +58,7 @@ export const DeleteArticleButton = ({
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete article?</AlertDialogTitle>
|
<AlertDialogTitle>Delete article?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This will permanently delete “{articleTitle}
|
This will permanently delete “{title}
|
||||||
”. This action cannot be undone.
|
”. This action cannot be undone.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
|
import { unstable_cache } from 'next/cache';
|
||||||
|
|
||||||
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
||||||
export type TypedResult<T> = Result<T, Error>;
|
export type TypedResult<T> = Result<T, Error>;
|
||||||
|
|
||||||
export function wrapBlocking<
|
|
||||||
F extends (...args: never[]) => unknown,
|
|
||||||
E = unknown,
|
|
||||||
>(
|
|
||||||
fn: F,
|
|
||||||
mapError: (e: unknown) => E = (e) => e as E
|
|
||||||
): (...args: Parameters<F>) => Result<ReturnType<F>, E> {
|
|
||||||
return (...args) => {
|
|
||||||
try {
|
|
||||||
return { ok: true, value: fn(...args) as ReturnType<F> };
|
|
||||||
} catch (e) {
|
|
||||||
return { ok: false, error: mapError(e) };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrap<
|
export function wrap<
|
||||||
F extends (...args: never[]) => Promise<unknown>,
|
F extends (...args: never[]) => Promise<unknown>,
|
||||||
E = unknown,
|
E = unknown,
|
||||||
@@ -35,3 +21,43 @@ export function wrap<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wrapCached<
|
||||||
|
F extends (...args: never[]) => Promise<unknown>,
|
||||||
|
E = unknown,
|
||||||
|
>(
|
||||||
|
fn: F,
|
||||||
|
options: {
|
||||||
|
key: (...args: Parameters<F>) => string[];
|
||||||
|
tags?: (...args: Parameters<F>) => string[];
|
||||||
|
revalidate?: number;
|
||||||
|
},
|
||||||
|
mapError: (e: unknown) => E = (e) => e as E
|
||||||
|
): (...args: Parameters<F>) => Promise<Result<Awaited<ReturnType<F>>, E>> {
|
||||||
|
return async (...args: Parameters<F>) => {
|
||||||
|
try {
|
||||||
|
const cachedFn = unstable_cache(
|
||||||
|
async (...innerArgs: Parameters<F>) => {
|
||||||
|
return await fn(...innerArgs);
|
||||||
|
},
|
||||||
|
options.key(...args),
|
||||||
|
{
|
||||||
|
tags: options.tags?.(...args),
|
||||||
|
revalidate: options.revalidate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = await cachedFn(...args);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: value as Awaited<ReturnType<F>>,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: mapError(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
type HexChar =
|
import { z } from 'zod';
|
||||||
| '0'
|
|
||||||
| '1'
|
|
||||||
| '2'
|
|
||||||
| '3'
|
|
||||||
| '4'
|
|
||||||
| '5'
|
|
||||||
| '6'
|
|
||||||
| '7'
|
|
||||||
| '8'
|
|
||||||
| '9'
|
|
||||||
| 'a'
|
|
||||||
| 'b'
|
|
||||||
| 'c'
|
|
||||||
| 'd'
|
|
||||||
| 'e'
|
|
||||||
| 'f';
|
|
||||||
type UUIDv4Segment<Length extends number> =
|
|
||||||
`${HexChar extends string ? HexChar : never}${string extends `${Length}` ? never : never}`; // Simplified for brevity
|
|
||||||
|
|
||||||
export type UUIDv4 =
|
export const UUIDv4 = z.uuid();
|
||||||
`${UUIDv4Segment<8>}-${UUIDv4Segment<4>}-4${UUIDv4Segment<3>}-${'8' | '9' | 'a' | 'b'}${UUIDv4Segment<3>}-${UUIDv4Segment<12>}`;
|
export type UUIDv4 = z.infer<typeof UUIDv4>;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { getArticlesPaginated } from '@/lib/feature/article/article.external';
|
|
||||||
import {
|
import {
|
||||||
CreateArticleModel,
|
CreateArticleModel,
|
||||||
UpdateArticleModel,
|
UpdateArticleModel,
|
||||||
} from '@/lib/feature/article/article.model';
|
} from '@/lib/feature/article/article.model';
|
||||||
import {
|
import {
|
||||||
deleteArticle,
|
deleteArticleByExternalId,
|
||||||
getArticleByExternalId,
|
getArticleByExternalId,
|
||||||
getArticleBySlug,
|
getArticleBySlug,
|
||||||
getArticlesByAuthorId,
|
getArticlesByAuthorId,
|
||||||
|
getArticlesPaginated,
|
||||||
saveArticle,
|
saveArticle,
|
||||||
updateArticle,
|
updateArticle,
|
||||||
} from '@/lib/feature/article/article.service';
|
} from '@/lib/feature/article/article.service';
|
||||||
@@ -259,7 +259,9 @@ describe('ArticleService', () => {
|
|||||||
if (!saveResult.ok) return;
|
if (!saveResult.ok) return;
|
||||||
expect(saveResult.value.id).toBeDefined();
|
expect(saveResult.value.id).toBeDefined();
|
||||||
|
|
||||||
const deleteResult = await deleteArticle(saveResult.value.id);
|
const deleteResult = await deleteArticleByExternalId(
|
||||||
|
saveResult.value.externalId
|
||||||
|
);
|
||||||
expect(deleteResult.ok).toBe(true);
|
expect(deleteResult.ok).toBe(true);
|
||||||
|
|
||||||
const getResult = await getArticleBySlug('article-to-delete');
|
const getResult = await getArticleBySlug('article-to-delete');
|
||||||
@@ -269,7 +271,7 @@ describe('ArticleService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('cannot delete non-existing article', async () => {
|
test('cannot delete non-existing article', async () => {
|
||||||
const result = await deleteArticle('9999');
|
const result = await deleteArticleByExternalId('9999');
|
||||||
|
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
if (result.ok) return;
|
if (result.ok) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user