diff --git a/src/app/(pages)/article/[slug]/page.tsx b/src/app/(pages)/article/[slug]/page.tsx
index d5867dd..773f7f6 100644
--- a/src/app/(pages)/article/[slug]/page.tsx
+++ b/src/app/(pages)/article/[slug]/page.tsx
@@ -1,5 +1,6 @@
import { getArticleBySlug } from '@/lib/feature/article/article.external';
import { ArrowLeftIcon, CalendarIcon, ClockIcon } from 'lucide-react';
+import { cacheTag } from 'next/cache';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
@@ -10,10 +11,6 @@ type ArticlePageProps = {
params: Promise<{ slug: string }>;
};
-type ArticleContentProps = {
- params: Promise<{ slug: string }>;
-};
-
function readingTime(text: string): number {
const words = text.trim().split(/\s+/).length;
return Math.max(1, Math.ceil(words / 200));
@@ -68,8 +65,11 @@ const ArticleContentSkeleton = () => (
);
-const ArticleContent = async ({ params }: ArticleContentProps) => {
- const { slug } = await params;
+const ArticleContent = async ({ slug }: { slug: string }) => {
+ 'use cache';
+
+ cacheTag(`article:slug:${slug}`);
+
const articleResult = await getArticleBySlug(slug);
if (!articleResult.ok) throw articleResult.error;
const article = articleResult.value;
@@ -144,10 +144,15 @@ const ArticleContent = async ({ params }: ArticleContentProps) => {
);
};
-const ArticlePage = ({ params }: ArticlePageProps) => {
+const ArticleContentWrapper = async ({ params }: ArticlePageProps) => {
+ const { slug } = await params;
+ return ;
+};
+
+const ArticlePage = (props: ArticlePageProps) => {
return (
}>
-
+
);
};
diff --git a/src/app/(pages)/home/page.tsx b/src/app/(pages)/home/page.tsx
index e0d6fb3..b101b94 100644
--- a/src/app/(pages)/home/page.tsx
+++ b/src/app/(pages)/home/page.tsx
@@ -1,15 +1,23 @@
import { ArticleList } from '@/ui/components/internal/article/article-list';
+import { ArticleListSkeleton } from '@/ui/components/internal/article/article-list-skeleton';
+import { Suspense } from 'react';
const DEFAULT_PAGE_SIZE = 4;
type HomeProps = {
- searchParams?: { page?: string; pageSize?: string };
+ searchParams?: Promise<{ page?: string; pageSize?: string }>;
};
-const Home = async ({ searchParams }: HomeProps) => {
- const page = Number(searchParams?.page) || 0;
- const pageSize = Number(searchParams?.pageSize) || DEFAULT_PAGE_SIZE;
+const ArticleListWrapper = async ({ searchParams }: HomeProps) => {
+ const params = await searchParams;
+ const page = Number(params?.page) || 1;
+ const pageSize = Number(params?.pageSize) || DEFAULT_PAGE_SIZE;
+
+ return ;
+};
+
+const Home = async (props: HomeProps) => {
return (
@@ -20,7 +28,13 @@ const Home = async ({ searchParams }: HomeProps) => {
Latest Articles
-
+
+ }
+ >
+
+
);
};
diff --git a/src/lib/feature/article/article.external.ts b/src/lib/feature/article/article.external.ts
index c6c98b0..2e5cea2 100644
--- a/src/lib/feature/article/article.external.ts
+++ b/src/lib/feature/article/article.external.ts
@@ -8,42 +8,34 @@ import {
} from '@/lib/feature/article/article.model';
import * as service from '@/lib/feature/article/article.service';
import { getSessionData } from '@/lib/session/session-storage';
-import { TypedResult, wrap, wrapCached } from '@/utils/types/results';
+import { TypedResult, wrap } from '@/utils/types/results';
import { UUIDv4 } from '@/utils/types/uuid';
import { revalidateTag } from 'next/cache';
export const getArticleByExternalId: (
externalId: UUIDv4
-) => Promise> = wrapCached(
+) => Promise> = wrap(
async (externalId: UUIDv4): Promise => {
const result = await service.getArticleByExternalId(externalId);
if (!result.ok) throw result.error;
return result.value;
- },
- {
- key: (id) => [`article:${id}`],
- tags: (id) => ['articles', `article:${id}`],
}
);
export const getArticleBySlug: (
slug: string
-) => Promise> = wrapCached(
+) => Promise> = wrap(
async (slug: string): Promise => {
const result = await service.getArticleBySlug(slug);
if (!result.ok) throw result.error;
return result.value;
- },
- {
- key: (slug) => [`article:slug:${slug}`],
- tags: (slug) => ['articles', `article:slug:${slug}`],
}
);
export const getArticlesPaginated: (
page?: number,
pageSize?: number
-) => Promise> = wrapCached(
+) => Promise> = wrap(
async (
page: number = 1,
pageSize: number = 10
@@ -51,13 +43,6 @@ export const getArticlesPaginated: (
const result = await service.getArticlesPaginated(page, pageSize);
if (!result.ok) throw result.error;
return result.value;
- },
- {
- key: (page, pageSize) => [`articles:page:${page}:${pageSize}`],
- tags: (page, pageSize) => [
- 'articles',
- `articles:page:${page}-${pageSize}`,
- ],
}
);
@@ -81,12 +66,12 @@ export const saveArticle: (
}
);
-export const updateArticle: (
- articleId: string,
+export const updateArticleByExternalId: (
+ externalId: UUIDv4,
article: UpdateArticleModel
) => Promise> = wrap(
async (
- articleId: string,
+ externalId: UUIDv4,
article: UpdateArticleModel
): Promise => {
const session = await getSessionData();
@@ -96,11 +81,14 @@ export const updateArticle: (
);
}
- const result = await service.updateArticle(articleId, article);
+ const result = await service.updateArticleByExternalId(
+ externalId,
+ article
+ );
if (!result.ok) throw result.error;
revalidateTag('articles', 'max');
- revalidateTag(`article:${articleId}`, 'max');
+ revalidateTag(`article:${externalId}`, 'max');
revalidateTag(`article:slug:${result.value.slug}`, 'max');
return result.value;
diff --git a/src/lib/feature/article/article.model.ts b/src/lib/feature/article/article.model.ts
index 7851c97..5c5d1dc 100644
--- a/src/lib/feature/article/article.model.ts
+++ b/src/lib/feature/article/article.model.ts
@@ -21,7 +21,6 @@ export const UpdateArticleModel = z.object({
export type UpdateArticleModel = z.infer;
export const ArticleModel = z.object({
- id: z.string(),
title: z.string(),
slug: z.string(),
description: z.string(),
diff --git a/src/lib/feature/article/article.service.ts b/src/lib/feature/article/article.service.ts
index 2121fce..9acc668 100644
--- a/src/lib/feature/article/article.service.ts
+++ b/src/lib/feature/article/article.service.ts
@@ -13,7 +13,6 @@ export const articleEntityToModel = (
articleEntity: ArticleEntity
): ArticleModel => {
return {
- id: articleEntity.id,
title: articleEntity.title,
slug: articleEntity.slug,
description: articleEntity.description,
@@ -123,21 +122,21 @@ export const saveArticle: (
);
/** Updates an existing article in the database. */
-export const updateArticle: (
- articleId: string,
+export const updateArticleByExternalId: (
+ externalId: string,
article: UpdateArticleModel
) => Promise> = wrap(
async (
- articleId: string,
+ externalId: string,
article: UpdateArticleModel
): Promise => {
const articleRepository = await getRepository(ArticleEntity);
const existingArticle = await articleRepository.findOneBy({
- id: articleId,
+ externalId: externalId,
});
if (!existingArticle) {
- throw new Error(`Article with ID ${articleId} not found`);
+ throw new Error(`Article with ID ${externalId} not found`);
}
if (!!article.title) existingArticle.title = article.title;
diff --git a/src/ui/components/internal/update-article-form.tsx b/src/ui/components/internal/update-article-form.tsx
index 9d8ae78..33ff894 100644
--- a/src/ui/components/internal/update-article-form.tsx
+++ b/src/ui/components/internal/update-article-form.tsx
@@ -1,6 +1,6 @@
'use client';
-import { updateArticle } from '@/lib/feature/article/article.external';
+import { updateArticleByExternalId } from '@/lib/feature/article/article.external';
import { ArticleModel } from '@/lib/feature/article/article.model';
import { uploadFile } from '@/lib/storage/storage.utils';
import { FileUploadField } from '@/ui/components/internal/file-upload-field';
@@ -97,7 +97,10 @@ export const UpdateArticleForm = ({ article }: UpdateArticleFormProps) => {
const handleFormSubmit = useCallback(
async (data: z.infer) => {
- const result = await updateArticle(article.id, data);
+ const result = await updateArticleByExternalId(
+ article.externalId,
+ data
+ );
if (!result.ok) {
toast.error('Failed to update article', {
description: result.error.message,
@@ -110,7 +113,7 @@ export const UpdateArticleForm = ({ article }: UpdateArticleFormProps) => {
position: 'bottom-right',
});
},
- [article.id]
+ [article.externalId]
);
const handleCoverImageFileChange = useCallback(
diff --git a/tests/lib/feature/article/article.service.test.ts b/tests/lib/feature/article/article.service.test.ts
index 11267cb..13aceb4 100644
--- a/tests/lib/feature/article/article.service.test.ts
+++ b/tests/lib/feature/article/article.service.test.ts
@@ -9,7 +9,7 @@ import {
getArticlesByAuthorId,
getArticlesPaginated,
saveArticle,
- updateArticle,
+ updateArticleByExternalId,
} from '@/lib/feature/article/article.service';
import { CreateUserModel } from '@/lib/feature/user/user.model';
import { saveUser } from '@/lib/feature/user/user.service';
@@ -56,7 +56,6 @@ describe('ArticleService', () => {
expect(result.ok).toBe(true);
if (!result.ok) return;
- expect(result.value.id).toBeDefined();
expect(result.value.title).toBe(articleToSave.title);
expect(result.value.slug).toBe(articleToSave.slug);
expect(result.value.description).toBe(articleToSave.description);
@@ -136,7 +135,6 @@ describe('ArticleService', () => {
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value).toBeDefined();
- expect(result.value?.id).toBe(article!.id);
expect(result.value?.title).toBe(article!.title);
expect(result.value?.slug).toBe(article!.slug);
expect(result.value?.externalId).toBe(article!.externalId);
@@ -219,12 +217,14 @@ describe('ArticleService', () => {
const article = slugResult.value;
expect(article).toBeDefined();
- const result = await updateArticle(article!.id, dataToUpdate);
+ const result = await updateArticleByExternalId(
+ article!.externalId,
+ dataToUpdate
+ );
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value).toBeDefined();
- expect(result.value.id).toBe(article!.id);
expect(result.value.title).toBe(dataToUpdate.title);
expect(result.value.description).toBe(dataToUpdate.description);
expect(result.value.slug).toBe(article!.slug);
@@ -237,7 +237,7 @@ describe('ArticleService', () => {
title: 'Updated Article Title',
};
- const result = await updateArticle('9999', dataToUpdate);
+ const result = await updateArticleByExternalId('9999', dataToUpdate);
expect(result.ok).toBe(false);
if (result.ok) return;
@@ -257,7 +257,7 @@ describe('ArticleService', () => {
const saveResult = await saveArticle(articleToSave);
expect(saveResult.ok).toBe(true);
if (!saveResult.ok) return;
- expect(saveResult.value.id).toBeDefined();
+ expect(saveResult.value.externalId).toBeDefined();
const deleteResult = await deleteArticleByExternalId(
saveResult.value.externalId
diff --git a/tests/lib/storage/storage.s3.test.ts b/tests/lib/storage/storage.s3.test.ts
index 12fe89d..08819d8 100644
--- a/tests/lib/storage/storage.s3.test.ts
+++ b/tests/lib/storage/storage.s3.test.ts
@@ -1,20 +1,15 @@
-import { S3StorageAdapter, S3StorageConfig } from '@/lib/storage/storage.adapter';
-import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
+import {
+ S3StorageAdapter,
+ S3StorageConfig,
+} from '@/lib/storage/storage.adapter';
+import {
+ DeleteObjectCommand,
+ PutObjectCommand,
+ S3Client,
+} from '@aws-sdk/client-s3';
import * as presigner from '@aws-sdk/s3-request-presigner';
import { mockClient } from 'aws-sdk-client-mock';
-
-
-
-
-
-
-
-
-
-
-
-
jest.mock('@aws-sdk/s3-request-presigner');
describe('S3StorageAdapter', () => {