Compare commits

..

2 Commits

Author SHA1 Message Date
e2960027f2 refactor: update article service methods to use external ID and improve caching
All checks were successful
Build and Test / run-test (20.x) (push) Successful in 2m5s
2026-04-17 01:43:31 -03:00
93d66315a1 refactor: update Next.js and Clerk dependencies to latest versions 2026-04-17 01:01:21 -03:00
10 changed files with 138 additions and 135 deletions

132
package-lock.json generated
View File

@@ -20,7 +20,7 @@
"iron-session": "^8.0.4",
"jotai": "^2.19.0",
"lucide-react": "^1.7.0",
"next": "16.2.1",
"next": "^16.2.4",
"next-themes": "^0.4.6",
"pg": "^8.20.0",
"radix-ui": "^1.4.3",
@@ -1585,12 +1585,12 @@
"license": "MIT"
},
"node_modules/@clerk/backend": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.2.3.tgz",
"integrity": "sha512-I3YLnSioYFG+EVFBYm0ilN28+FC8H+hkqMgB5Pdl7AcotQOn3JhiZMqLel2H0P390p8FEJKQNnrvXk3BemeKKQ==",
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.2.12.tgz",
"integrity": "sha512-pTuD3+3IvLrjx9XMYdbqpttTltrWc7npxkOIDzhtID5/+IOomxZAzsnxU1G1hvOeBoR1Jl68ZgKQw66aZ+DRFQ==",
"license": "MIT",
"dependencies": {
"@clerk/shared": "^4.3.2",
"@clerk/shared": "^4.8.2",
"standardwebhooks": "^1.0.0",
"tslib": "2.8.1"
},
@@ -1599,14 +1599,14 @@
}
},
"node_modules/@clerk/nextjs": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-7.0.7.tgz",
"integrity": "sha512-Iqg4q0ns1LZZrAdC66r/QUFMY+Rs3HAJcAb/IR0uFBj7ZAZusxdVKMmNkZP9UP6sk3OOorCsJTdE0rTMoXD2YQ==",
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-7.2.2.tgz",
"integrity": "sha512-kFQ+sXD5qAxL8C53hDgpKJT+KithlOFDlkMK5Bwv/YNFy/Sf5Tzkxdh4y/AfAgJlDU0ksvu0Hn7mtB2QT8t9bg==",
"license": "MIT",
"dependencies": {
"@clerk/backend": "^3.2.3",
"@clerk/react": "^6.1.3",
"@clerk/shared": "^4.3.2",
"@clerk/backend": "^3.2.12",
"@clerk/react": "^6.4.2",
"@clerk/shared": "^4.8.2",
"server-only": "0.0.1",
"tslib": "2.8.1"
},
@@ -1620,12 +1620,12 @@
}
},
"node_modules/@clerk/react": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.1.3.tgz",
"integrity": "sha512-9t5C8eM5cTmOmpBO5nb8FDA40biQqeQLUW+cVwAE0t5hnGRwiC6mSv83vqHg+9qQBqtliR013BGVjpCz53gVCA==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.4.2.tgz",
"integrity": "sha512-l43pon5wcM0n4e3gnRP7MWdvAXAa3M7BOF6aeNwEuITxgy6MU6ypej9tPeGmXxPicL/Hd6E/VRgiEipEKDqyCQ==",
"license": "MIT",
"dependencies": {
"@clerk/shared": "^4.3.2",
"@clerk/shared": "^4.8.2",
"tslib": "2.8.1"
},
"engines": {
@@ -1637,9 +1637,9 @@
}
},
"node_modules/@clerk/shared": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.3.2.tgz",
"integrity": "sha512-tYYzdY4Fxb02TO4RHoLRFzEjXJn0iFDfoKhWtGyqf2AaIgkprTksunQtX0hnVssHMr3XD/E2S00Vrb+PzX3jCQ==",
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.8.2.tgz",
"integrity": "sha512-kBFDNeLdiNZkOHavTkOB00NMuqsmOGgVtDlzCS0/yivxsCUZXk3AT6+UsTfeSBGV7QD80YxjeFMNSrpqnSv4Gg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -2164,9 +2164,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.11",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
"version": "1.19.14",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -3518,9 +3518,9 @@
}
},
"node_modules/@next/env": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz",
"integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz",
"integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -3534,9 +3534,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz",
"integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz",
"integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==",
"cpu": [
"arm64"
],
@@ -3550,9 +3550,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz",
"integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz",
"integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==",
"cpu": [
"x64"
],
@@ -3566,9 +3566,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz",
"integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz",
"integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==",
"cpu": [
"arm64"
],
@@ -3582,9 +3582,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz",
"integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz",
"integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==",
"cpu": [
"arm64"
],
@@ -3598,9 +3598,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz",
"integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz",
"integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==",
"cpu": [
"x64"
],
@@ -3614,9 +3614,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz",
"integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz",
"integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==",
"cpu": [
"x64"
],
@@ -3630,9 +3630,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz",
"integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz",
"integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==",
"cpu": [
"arm64"
],
@@ -3646,9 +3646,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz",
"integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
"integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
"cpu": [
"x64"
],
@@ -11598,9 +11598,9 @@
}
},
"node_modules/hono": {
"version": "4.12.9",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
"integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -15239,12 +15239,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz",
"integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==",
"version": "16.2.4",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz",
"integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==",
"license": "MIT",
"dependencies": {
"@next/env": "16.2.1",
"@next/env": "16.2.4",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -15258,14 +15258,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.1",
"@next/swc-darwin-x64": "16.2.1",
"@next/swc-linux-arm64-gnu": "16.2.1",
"@next/swc-linux-arm64-musl": "16.2.1",
"@next/swc-linux-x64-gnu": "16.2.1",
"@next/swc-linux-x64-musl": "16.2.1",
"@next/swc-win32-arm64-msvc": "16.2.1",
"@next/swc-win32-x64-msvc": "16.2.1",
"@next/swc-darwin-arm64": "16.2.4",
"@next/swc-darwin-x64": "16.2.4",
"@next/swc-linux-arm64-gnu": "16.2.4",
"@next/swc-linux-arm64-musl": "16.2.4",
"@next/swc-linux-x64-gnu": "16.2.4",
"@next/swc-linux-x64-musl": "16.2.4",
"@next/swc-win32-arm64-msvc": "16.2.4",
"@next/swc-win32-x64-msvc": "16.2.4",
"sharp": "^0.34.5"
},
"peerDependencies": {
@@ -16513,9 +16513,9 @@
}
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
"dev": true,
"hasInstallScript": true,
"license": "BSD-3-Clause",

View File

@@ -29,7 +29,7 @@
"iron-session": "^8.0.4",
"jotai": "^2.19.0",
"lucide-react": "^1.7.0",
"next": "16.2.1",
"next": "^16.2.4",
"next-themes": "^0.4.6",
"pg": "^8.20.0",
"radix-ui": "^1.4.3",

View File

@@ -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 = () => (
</article>
);
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 <ArticleContent slug={slug} />;
};
const ArticlePage = (props: ArticlePageProps) => {
return (
<Suspense fallback={<ArticleContentSkeleton />}>
<ArticleContent params={params} />
<ArticleContentWrapper {...props} />
</Suspense>
);
};

View File

@@ -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 <ArticleList page={page} pageSize={pageSize} />;
};
const Home = async (props: HomeProps) => {
return (
<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'>
@@ -20,7 +28,13 @@ const Home = async ({ searchParams }: HomeProps) => {
Latest Articles
</h1>
</div>
<ArticleList page={page} pageSize={pageSize} />
<Suspense
fallback={
<ArticleListSkeleton skeletonSize={DEFAULT_PAGE_SIZE} />
}
>
<ArticleListWrapper {...props} />
</Suspense>
</div>
);
};

View File

@@ -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<TypedResult<ArticleModel | null>> = wrapCached(
) => Promise<TypedResult<ArticleModel | null>> = wrap(
async (externalId: UUIDv4): Promise<ArticleModel | null> => {
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<TypedResult<ArticleModel | null>> = wrapCached(
) => Promise<TypedResult<ArticleModel | null>> = wrap(
async (slug: string): Promise<ArticleModel | null> => {
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<TypedResult<PaginatedArticlesResult>> = wrapCached(
) => Promise<TypedResult<PaginatedArticlesResult>> = 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<TypedResult<ArticleModel>> = wrap(
async (
articleId: string,
externalId: UUIDv4,
article: UpdateArticleModel
): Promise<ArticleModel> => {
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;

View File

@@ -21,7 +21,6 @@ export const UpdateArticleModel = z.object({
export type UpdateArticleModel = z.infer<typeof UpdateArticleModel>;
export const ArticleModel = z.object({
id: z.string(),
title: z.string(),
slug: z.string(),
description: z.string(),

View File

@@ -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<TypedResult<ArticleModel>> = wrap(
async (
articleId: string,
externalId: string,
article: UpdateArticleModel
): Promise<ArticleModel> => {
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;

View File

@@ -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<typeof formSchema>) => {
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(

View File

@@ -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

View File

@@ -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', () => {