feat: wrap article and user service functions with TypedResult for improved error handling

This commit is contained in:
2026-04-11 01:48:09 -03:00
parent 94e8058880
commit af17b6dc5a
10 changed files with 363 additions and 240 deletions

View File

@@ -70,7 +70,9 @@ const ArticleContentSkeleton = () => (
const ArticleContent = async ({ params }: ArticleContentProps) => { const ArticleContent = async ({ params }: ArticleContentProps) => {
const { slug } = await params; const { slug } = await params;
const article = await getArticleBySlug(slug); const articleResult = await getArticleBySlug(slug);
if (!articleResult.ok) throw articleResult.error;
const article = articleResult.value;
if (!article) notFound(); if (!article) notFound();

View File

@@ -45,11 +45,9 @@ const ArticleList = async ({ searchParams }: ArticleListProps) => {
const page = Math.max(1, Number(pageParam) || 1); const page = Math.max(1, Number(pageParam) || 1);
const pageSize = Number(pageSizeParam) || PAGE_SIZE; const pageSize = Number(pageSizeParam) || PAGE_SIZE;
const { const paginationResult = await getArticlesPaginated(page, pageSize);
data: articles, if (!paginationResult.ok) throw paginationResult.error;
totalPages, const { data: articles, totalPages, total } = paginationResult.value;
total,
} = await getArticlesPaginated(page, pageSize);
return ( return (
<> <>

View File

@@ -15,8 +15,12 @@ export async function GET() {
redirect('/'); redirect('/');
} }
const syncedUser = await syncUser(parsedClaims); const syncResult = await syncUser(parsedClaims);
if (!syncResult.ok) {
await setSessionData('user', syncedUser); console.error(syncResult.error);
redirect('/');
}
await setSessionData('user', syncResult.value);
redirect('/'); redirect('/');
} }

View File

@@ -8,28 +8,35 @@ 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 { UUIDv4 } from '@/utils/types/uuid'; import { UUIDv4 } from '@/utils/types/uuid';
export const getArticleByExternalId = async ( const _getArticleByExternalId = async (
externalId: UUIDv4 externalId: UUIDv4
): Promise<ArticleModel | null> => { ): Promise<ArticleModel | null> => {
return await service.getArticleByExternalId(externalId); const result = await service.getArticleByExternalId(externalId);
if (!result.ok) throw result.error;
return result.value;
}; };
export const getArticleBySlug = async ( const _getArticleBySlug = async (
slug: string slug: string
): Promise<ArticleModel | null> => { ): Promise<ArticleModel | null> => {
return await service.getArticleBySlug(slug); const result = await service.getArticleBySlug(slug);
if (!result.ok) throw result.error;
return result.value;
}; };
export const getArticlesPaginated = async ( const _getArticlesPaginated = async (
page: number = 1, page: number = 1,
pageSize: number = 10 pageSize: number = 10
): Promise<PaginatedArticlesResult> => { ): Promise<PaginatedArticlesResult> => {
return await service.getArticlesPaginated(page, pageSize); const result = await service.getArticlesPaginated(page, pageSize);
if (!result.ok) throw result.error;
return result.value;
}; };
export const saveArticle = async ( const _saveArticle = async (
article: CreateArticleModel article: CreateArticleModel
): Promise<ArticleModel> => { ): Promise<ArticleModel> => {
const session = await getSessionData(); const session = await getSessionData();
@@ -38,10 +45,12 @@ export const saveArticle = async (
} }
article.authorId = session.user.id; article.authorId = session.user.id;
return await service.saveArticle(article); const result = await service.saveArticle(article);
if (!result.ok) throw result.error;
return result.value;
}; };
export const updateArticle = async ( const _updateArticle = async (
articleId: string, articleId: string,
article: UpdateArticleModel article: UpdateArticleModel
): Promise<ArticleModel> => { ): Promise<ArticleModel> => {
@@ -49,13 +58,45 @@ export const updateArticle = async (
if (!session || !session?.user || session?.user.role !== 'admin') { if (!session || !session?.user || session?.user.role !== 'admin') {
throw new Error('Unauthorized: Only admin users can save articles.'); throw new Error('Unauthorized: Only admin users can save articles.');
} }
return await service.updateArticle(articleId, article);
const result = await service.updateArticle(articleId, article);
if (!result.ok) throw result.error;
return result.value;
}; };
export const deleteArticle = async (articleId: string): Promise<void> => { const _deleteArticle = async (articleId: string): 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('Unauthorized: Only admin users can delete articles.'); throw new Error('Unauthorized: Only admin users can delete articles.');
} }
await service.deleteArticle(articleId);
const result = await service.deleteArticle(articleId);
if (!result.ok) throw result.error;
}; };
export const getArticleByExternalId: (
externalId: UUIDv4
) => Promise<TypedResult<ArticleModel | null>> = wrap(_getArticleByExternalId);
export const getArticleBySlug: (
slug: string
) => Promise<TypedResult<ArticleModel | null>> = wrap(_getArticleBySlug);
export const getArticlesPaginated: (
page?: number,
pageSize?: number
) => Promise<TypedResult<PaginatedArticlesResult>> = wrap(
_getArticlesPaginated
);
export const saveArticle: (
article: CreateArticleModel
) => Promise<TypedResult<ArticleModel>> = wrap(_saveArticle);
export const updateArticle: (
articleId: string,
article: UpdateArticleModel
) => Promise<TypedResult<ArticleModel>> = wrap(_updateArticle);
export const deleteArticle: (articleId: string) => Promise<TypedResult<void>> =
wrap(_deleteArticle);

View File

@@ -6,6 +6,7 @@ import {
PaginatedArticlesResult, PaginatedArticlesResult,
UpdateArticleModel, UpdateArticleModel,
} from '@/lib/feature/article/article.model'; } from '@/lib/feature/article/article.model';
import { TypedResult, wrap } from '@/utils/types/results';
import { UUIDv4 } from '@/utils/types/uuid'; import { UUIDv4 } from '@/utils/types/uuid';
export const articleEntityToModel = ( export const articleEntityToModel = (
@@ -25,11 +26,7 @@ export const articleEntityToModel = (
}; };
}; };
/** Retrieves an artible by its external ID. const _getArticleByExternalId = async (
* @param externalId - The external ID of the article to retrieve.
* @returns {Promise<ArticleModel | null>} The article model if found, otherwise null.
* */
export const getArticleByExternalId = async (
externalId: UUIDv4 externalId: UUIDv4
): Promise<ArticleModel | null> => { ): Promise<ArticleModel | null> => {
const articleRepository = await getRepository(ArticleEntity); const articleRepository = await getRepository(ArticleEntity);
@@ -45,12 +42,7 @@ export const getArticleByExternalId = async (
return articleEntityToModel(articleEntity); return articleEntityToModel(articleEntity);
}; };
/** const _getArticleBySlug = async (
* Retrieves an article by its slug.
* @param slug - The slug of the article to retrieve.
* @returns {Promise<ArticleModel | null>} The article model if found, otherwise null.
*/
export const getArticleBySlug = async (
slug: string slug: string
): Promise<ArticleModel | null> => { ): Promise<ArticleModel | null> => {
const articleRepository = await getRepository(ArticleEntity); const articleRepository = await getRepository(ArticleEntity);
@@ -64,12 +56,7 @@ export const getArticleBySlug = async (
return articleEntityToModel(articleEntity); return articleEntityToModel(articleEntity);
}; };
/** const _getArticlesByAuthorId = async (
* Retrieves all articles by a given author ID.
* @param authorId - The ID of the author.
* @returns {Promise<ArticleModel[]>} A list of article models.
*/
export const getArticlesByAuthorId = async (
authorId: string authorId: string
): Promise<ArticleModel[]> => { ): Promise<ArticleModel[]> => {
const articleRepository = await getRepository(ArticleEntity); const articleRepository = await getRepository(ArticleEntity);
@@ -79,13 +66,7 @@ export const getArticlesByAuthorId = async (
return articleEntities.map(articleEntityToModel); return articleEntities.map(articleEntityToModel);
}; };
/** const _getArticlesPaginated = async (
* Retrieves a paginated list of articles ordered by creation date descending.
* @param page - The page number (1-based).
* @param pageSize - The number of articles per page.
* @returns {Promise<PaginatedArticlesResult>} The paginated result.
*/
export const getArticlesPaginated = async (
page: number = 1, page: number = 1,
pageSize: number = 10 pageSize: number = 10
): Promise<PaginatedArticlesResult> => { ): Promise<PaginatedArticlesResult> => {
@@ -106,13 +87,7 @@ export const getArticlesPaginated = async (
}; };
}; };
/** const _saveArticle = async (
* Saves a new article to the database.
* @param article - The article data to save.
* @returns {Promise<ArticleModel>} The saved article model.
* @throws {Error} If an article with the same slug already exists.
*/
export const saveArticle = async (
article: CreateArticleModel article: CreateArticleModel
): Promise<ArticleModel> => { ): Promise<ArticleModel> => {
const articleRepository = await getRepository(ArticleEntity); const articleRepository = await getRepository(ArticleEntity);
@@ -129,14 +104,7 @@ export const saveArticle = async (
return articleEntityToModel(await articleRepository.save(newArticle)); return articleEntityToModel(await articleRepository.save(newArticle));
}; };
/** const _updateArticle = async (
* Updates an existing article in the database.
* @param articleId - The ID of the article to update.
* @param article - The new article data.
* @returns {Promise<ArticleModel>} The updated article model.
* @throws {Error} If the article with the given ID does not exist.
*/
export const updateArticle = async (
articleId: string, articleId: string,
article: UpdateArticleModel article: UpdateArticleModel
): Promise<ArticleModel> => { ): Promise<ArticleModel> => {
@@ -160,12 +128,7 @@ export const updateArticle = async (
return articleEntityToModel(await articleRepository.save(existingArticle)); return articleEntityToModel(await articleRepository.save(existingArticle));
}; };
/** const _deleteArticle = async (articleId: string): Promise<void> => {
* Deletes an article from the database.
* @param articleId - The ID of the article to delete.
* @throws {Error} If the article with the given ID does not exist.
*/
export const deleteArticle = async (articleId: string): Promise<void> => {
const articleRepository = await getRepository(ArticleEntity); const articleRepository = await getRepository(ArticleEntity);
const existingArticle = await articleRepository.findOneBy({ const existingArticle = await articleRepository.findOneBy({
@@ -177,3 +140,41 @@ export const deleteArticle = async (articleId: string): Promise<void> => {
await articleRepository.remove(existingArticle); await articleRepository.remove(existingArticle);
}; };
/** Retrieves an article by its external ID. */
export const getArticleByExternalId: (
externalId: UUIDv4
) => Promise<TypedResult<ArticleModel | null>> = wrap(_getArticleByExternalId);
/** Retrieves an article by its slug. */
export const getArticleBySlug: (
slug: string
) => Promise<TypedResult<ArticleModel | null>> = wrap(_getArticleBySlug);
/** Retrieves all articles by a given author ID. */
export const getArticlesByAuthorId: (
authorId: string
) => Promise<TypedResult<ArticleModel[]>> = wrap(_getArticlesByAuthorId);
/** Retrieves a paginated list of articles ordered by creation date descending. */
export const getArticlesPaginated: (
page?: number,
pageSize?: number
) => Promise<TypedResult<PaginatedArticlesResult>> = wrap(
_getArticlesPaginated
);
/** Saves a new article to the database. */
export const saveArticle: (
article: CreateArticleModel
) => Promise<TypedResult<ArticleModel>> = wrap(_saveArticle);
/** Updates an existing article in the database. */
export const updateArticle: (
articleId: string,
article: UpdateArticleModel
) => Promise<TypedResult<ArticleModel>> = wrap(_updateArticle);
/** Deletes an article from the database. */
export const deleteArticle: (articleId: string) => Promise<TypedResult<void>> =
wrap(_deleteArticle);

View File

@@ -2,11 +2,15 @@
import { getRepository } from '@/lib/db/client'; import { getRepository } from '@/lib/db/client';
import { UserEntity } from '@/lib/db/entities'; import { UserEntity } from '@/lib/db/entities';
import { UserModel } from '@/lib/feature/user/user.model';
import { userEntityToModel } from '@/lib/feature/user/user.service'; import { userEntityToModel } from '@/lib/feature/user/user.service';
import { getSessionData } from '@/lib/session/session-storage'; import { getSessionData } from '@/lib/session/session-storage';
import { TypedResult, wrap } from '@/utils/types/results';
import { UUIDv4 } from '@/utils/types/uuid'; import { UUIDv4 } from '@/utils/types/uuid';
export const getUserByExternalId = async (externalId: UUIDv4) => { const _getUserByExternalId = async (
externalId: UUIDv4
): Promise<UserModel | null> => {
const sessionData = await getSessionData(); const sessionData = await getSessionData();
if ( if (
!sessionData || !sessionData ||
@@ -28,3 +32,7 @@ export const getUserByExternalId = async (externalId: UUIDv4) => {
return userEntityToModel(userEntity); return userEntityToModel(userEntity);
}; };
export const getUserByExternalId: (
externalId: UUIDv4
) => Promise<TypedResult<UserModel | null>> = wrap(_getUserByExternalId);

View File

@@ -6,6 +6,7 @@ import {
UpdateUserModel, UpdateUserModel,
UserModel, UserModel,
} from '@/lib/feature/user/user.model'; } from '@/lib/feature/user/user.model';
import { TypedResult, wrap } from '@/utils/types/results';
export const userEntityToModel = (userEntity: UserEntity): UserModel => { export const userEntityToModel = (userEntity: UserEntity): UserModel => {
return { return {
@@ -17,14 +18,7 @@ export const userEntityToModel = (userEntity: UserEntity): UserModel => {
}; };
}; };
/** const _getUserByEmail = async (email: string): Promise<UserModel | null> => {
* Retrieves a user by their email address.
* @param email - The email address of the user to retrieve.
* @returns {Promise<UserModel | null>} The user model if found, otherwise null.
*/
export const getUserByEmail = async (
email: string
): Promise<UserModel | null> => {
const userRepository = await getRepository(UserEntity); const userRepository = await getRepository(UserEntity);
const userEntity = await userRepository.findOneBy({ email }); const userEntity = await userRepository.findOneBy({ email });
@@ -36,13 +30,7 @@ export const getUserByEmail = async (
return userEntityToModel(userEntity); return userEntityToModel(userEntity);
}; };
/** const _saveUser = async (user: CreateUserModel): Promise<UserModel> => {
* Saves a new user to the database.
* @param user - The user data to save.
* @returns {Promise<UserModel>} The saved user model.
* @throws {Error} If a user with the same email already exists.
*/
export const saveUser = async (user: CreateUserModel): Promise<UserModel> => {
const userRepository = await getRepository(UserEntity); const userRepository = await getRepository(UserEntity);
if (!!(await userRepository.findOneBy({ email: user.email }))) { if (!!(await userRepository.findOneBy({ email: user.email }))) {
@@ -53,14 +41,7 @@ export const saveUser = async (user: CreateUserModel): Promise<UserModel> => {
return userEntityToModel(await userRepository.save(newUser)); return userEntityToModel(await userRepository.save(newUser));
}; };
/** const _updateUser = async (
* Updates an existing user in the database.
* @param userId - The ID of the user to update.
* @param user - The new user data.
* @returns {Promise<UserModel>} The updated user model.
* @throws {Error} If the user with the given ID does not exist.
*/
export const updateUser = async (
userId: string, userId: string,
user: UpdateUserModel user: UpdateUserModel
): Promise<UserModel> => { ): Promise<UserModel> => {
@@ -78,33 +59,55 @@ export const updateUser = async (
return userEntityToModel(await userRepository.save(existingUser)); return userEntityToModel(await userRepository.save(existingUser));
}; };
/** const _syncUser = async (sessionClaims: SessionClaims): Promise<UserModel> => {
* Synchronizes a user with the database.
* If the user already exists, it skips saving and returns the existing user.
* If the user does not exist, it creates a new user record.
* @returns {Promise<UserModel>} The synchronized user model.
* @throws {Error} If the user email is not provided or if there is an issue
* saving the user.
* @param sessionClaims Session Claims from the Auth Provider
*/
export const syncUser = async (
sessionClaims: SessionClaims
): Promise<UserModel> => {
const { full_name, email } = sessionClaims.user; const { full_name, email } = sessionClaims.user;
const role = sessionClaims.user.public_metadata.role; const role = sessionClaims.user.public_metadata.role;
const existingUser = await getUserByEmail(email); const existingUser = await _getUserByEmail(email);
if (!existingUser) { if (!existingUser) {
return await saveUser({ return await _saveUser({
name: full_name, name: full_name,
email: email, email: email,
role: role, role: role,
}); });
} }
return await updateUser(existingUser.id, { return await _updateUser(existingUser.id, {
name: full_name, name: full_name,
email: existingUser.email, email: existingUser.email,
role: role, role: role,
}); });
}; };
/**
* Retrieves a user by their email address.
* @param email - The email address of the user to retrieve.
* @returns {Promise<TypedResult<UserModel | null>>} The user model if found, otherwise null.
*/
export const getUserByEmail = wrap(_getUserByEmail);
/**
* Saves a new user to the database.
* @param user - The user data to save.
* @returns {Promise<TypedResult<UserModel>>} The saved user model.
*/
export const saveUser = wrap(_saveUser);
/**
* Updates an existing user in the database.
* @param userId - The ID of the user to update.
* @param user - The new user data.
* @returns {Promise<TypedResult<UserModel>>} The updated user model.
*/
export const updateUser = wrap(_updateUser);
/**
* Synchronizes a user with the database.
* If the user already exists, updates it; if not, creates a new record.
* @param sessionClaims Session Claims from the Auth Provider
* @returns {Promise<TypedResult<UserModel>>} The synchronized user model.
*/
export const syncUser = wrap(_syncUser);
// Explicit re-export for TypeScript consumers who need the result type
export type { TypedResult };

View File

@@ -101,23 +101,20 @@ export const CreateArticleForm = () => {
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
async (data: z.infer<typeof formSchema>) => { async (data: z.infer<typeof formSchema>) => {
try {
const result = await saveArticle({ ...data }); const result = await saveArticle({ ...data });
if (!result.ok) {
toast.error('Failed to create article', {
description: result.error.message,
position: 'bottom-right',
});
return;
}
toast.success('Article created successfully!', { toast.success('Article created successfully!', {
description: `Article "${result.title}" has been created.`, description: `Article "${result.value.title}" has been created.`,
position: 'bottom-right', position: 'bottom-right',
}); });
form.reset(); form.reset();
resetFiles(); resetFiles();
} catch (error) {
toast.error('Failed to create article', {
description:
error instanceof Error
? error.message
: 'An error occurred',
position: 'bottom-right',
});
}
}, },
[form, resetFiles] [form, resetFiles]
); );

View File

@@ -34,7 +34,8 @@ describe('ArticleService', () => {
role: 'admin', role: 'admin',
}; };
const savedAuthor = await saveUser(author); const savedAuthor = await saveUser(author);
authorId = savedAuthor.id; if (!savedAuthor.ok) throw savedAuthor.error;
authorId = savedAuthor.value.id;
}, 1_000_000); }, 1_000_000);
afterAll(async () => { afterAll(async () => {
@@ -51,16 +52,18 @@ describe('ArticleService', () => {
authorId: authorId, authorId: authorId,
}; };
const savedArticle = await saveArticle(articleToSave); const result = await saveArticle(articleToSave);
expect(savedArticle.id).toBeDefined(); expect(result.ok).toBe(true);
expect(savedArticle.title).toBe(articleToSave.title); if (!result.ok) return;
expect(savedArticle.slug).toBe(articleToSave.slug); expect(result.value.id).toBeDefined();
expect(savedArticle.description).toBe(articleToSave.description); expect(result.value.title).toBe(articleToSave.title);
expect(savedArticle.coverImageUrl).toBe(articleToSave.coverImageUrl); expect(result.value.slug).toBe(articleToSave.slug);
expect(savedArticle.content).toBe(articleToSave.content); expect(result.value.description).toBe(articleToSave.description);
expect(savedArticle.authorId).toBe(authorId); expect(result.value.coverImageUrl).toBe(articleToSave.coverImageUrl);
expect(savedArticle.externalId).toBeDefined(); expect(result.value.content).toBe(articleToSave.content);
expect(result.value.authorId).toBe(authorId);
expect(result.value.externalId).toBeDefined();
}); });
test('cannot save article with existing slug', async () => { test('cannot save article with existing slug', async () => {
@@ -72,77 +75,99 @@ describe('ArticleService', () => {
content: 'Duplicate content.', content: 'Duplicate content.',
authorId: authorId, authorId: authorId,
}; };
await expect(saveArticle(articleToSave)).rejects.toThrow(
`Article with slug ${articleToSave.slug} already exists` const result = await saveArticle(articleToSave);
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toContain(articleToSave.slug);
}); });
test('can getArticleBySlug', async () => { test('can getArticleBySlug', async () => {
const article = await getArticleBySlug('test-article'); const result = await getArticleBySlug('test-article');
expect(article).toBeDefined(); expect(result.ok).toBe(true);
expect(article?.slug).toBe('test-article'); if (!result.ok) return;
expect(article?.title).toBe('Test Article'); expect(result.value).toBeDefined();
expect(article?.authorId).toBe(authorId); expect(result.value?.slug).toBe('test-article');
expect(article?.externalId).toBeDefined(); expect(result.value?.title).toBe('Test Article');
expect(result.value?.authorId).toBe(authorId);
expect(result.value?.externalId).toBeDefined();
}); });
test('cannot getArticleBySlug with non-existing slug', async () => { test('cannot getArticleBySlug with non-existing slug', async () => {
await expect(getArticleBySlug('non-existing-slug')).resolves.toBeNull(); const result = await getArticleBySlug('non-existing-slug');
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value).toBeNull();
}); });
test('can getArticlesByAuthorId', async () => { test('can getArticlesByAuthorId', async () => {
const articles = await getArticlesByAuthorId(authorId); const result = await getArticlesByAuthorId(authorId);
expect(articles).toBeDefined(); expect(result.ok).toBe(true);
expect(articles.length).toBeGreaterThanOrEqual(1); if (!result.ok) return;
expect(articles[0].authorId).toBe(authorId); expect(result.value).toBeDefined();
expect(result.value.length).toBeGreaterThanOrEqual(1);
expect(result.value[0].authorId).toBe(authorId);
}); });
test('getArticlesByAuthorId returns empty for non-existing author', async () => { test('getArticlesByAuthorId returns empty for non-existing author', async () => {
const articles = await getArticlesByAuthorId('9999'); const result = await getArticlesByAuthorId('9999');
expect(articles).toBeDefined(); expect(result.ok).toBe(true);
expect(articles.length).toBe(0); if (!result.ok) return;
expect(result.value).toBeDefined();
expect(result.value.length).toBe(0);
}); });
test('can getArticleByExternalId', async () => { test('can getArticleByExternalId', async () => {
const article = await getArticleBySlug('test-article'); const slugResult = await getArticleBySlug('test-article');
expect(slugResult.ok).toBe(true);
if (!slugResult.ok) return;
const article = slugResult.value;
expect(article).toBeDefined(); expect(article).toBeDefined();
const foundArticle = await getArticleByExternalId( const result = await getArticleByExternalId(
article!.externalId as UUIDv4 article!.externalId as UUIDv4
); );
expect(foundArticle).toBeDefined(); expect(result.ok).toBe(true);
expect(foundArticle?.id).toBe(article!.id); if (!result.ok) return;
expect(foundArticle?.title).toBe(article!.title); expect(result.value).toBeDefined();
expect(foundArticle?.slug).toBe(article!.slug); expect(result.value?.id).toBe(article!.id);
expect(foundArticle?.externalId).toBe(article!.externalId); expect(result.value?.title).toBe(article!.title);
expect(result.value?.slug).toBe(article!.slug);
expect(result.value?.externalId).toBe(article!.externalId);
}); });
test('getArticleByExternalId returns null for non-existing id', async () => { test('getArticleByExternalId returns null for non-existing id', async () => {
const result = await getArticleByExternalId( const result = await getArticleByExternalId(
'00000000-0000-4000-a000-000000000000' as UUIDv4 '00000000-0000-4000-a000-000000000000' as UUIDv4
); );
expect(result).toBeNull();
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value).toBeNull();
}); });
test('can getArticlesPaginated with defaults', async () => { test('can getArticlesPaginated with defaults', async () => {
const result = await getArticlesPaginated(); const result = await getArticlesPaginated();
expect(result).toBeDefined(); expect(result.ok).toBe(true);
expect(result.data.length).toBeGreaterThanOrEqual(1); if (!result.ok) return;
expect(result.page).toBe(1); expect(result.value.data.length).toBeGreaterThanOrEqual(1);
expect(result.pageSize).toBe(10); expect(result.value.page).toBe(1);
expect(result.total).toBeGreaterThanOrEqual(1); expect(result.value.pageSize).toBe(10);
expect(result.totalPages).toBeGreaterThanOrEqual(1); expect(result.value.total).toBeGreaterThanOrEqual(1);
expect(result.value.totalPages).toBeGreaterThanOrEqual(1);
}); });
test('can getArticlesPaginated with custom page size', async () => { test('can getArticlesPaginated with custom page size', async () => {
// Save extra articles to test pagination // Save extra articles to test pagination
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
await saveArticle({ const r = await saveArticle({
title: `Paginated Article ${i}`, title: `Paginated Article ${i}`,
slug: `paginated-article-${i}`, slug: `paginated-article-${i}`,
description: `Description ${i}`, description: `Description ${i}`,
@@ -150,29 +175,36 @@ describe('ArticleService', () => {
content: `Content ${i}`, content: `Content ${i}`,
authorId: authorId, authorId: authorId,
}); });
if (!r.ok) throw r.error;
} }
const firstPage = await getArticlesPaginated(1, 2); const firstPageResult = await getArticlesPaginated(1, 2);
expect(firstPage.data.length).toBe(2); expect(firstPageResult.ok).toBe(true);
expect(firstPage.page).toBe(1); if (!firstPageResult.ok) return;
expect(firstPage.pageSize).toBe(2); expect(firstPageResult.value.data.length).toBe(2);
expect(firstPage.total).toBeGreaterThanOrEqual(4); expect(firstPageResult.value.page).toBe(1);
expect(firstPage.totalPages).toBeGreaterThanOrEqual(2); expect(firstPageResult.value.pageSize).toBe(2);
expect(firstPageResult.value.total).toBeGreaterThanOrEqual(4);
expect(firstPageResult.value.totalPages).toBeGreaterThanOrEqual(2);
const secondPage = await getArticlesPaginated(2, 2); const secondPageResult = await getArticlesPaginated(2, 2);
expect(secondPage.data.length).toBe(2); expect(secondPageResult.ok).toBe(true);
expect(secondPage.page).toBe(2); if (!secondPageResult.ok) return;
expect(secondPage.pageSize).toBe(2); expect(secondPageResult.value.data.length).toBe(2);
expect(secondPageResult.value.page).toBe(2);
expect(secondPageResult.value.pageSize).toBe(2);
}); });
test('can getArticlesPaginated returns empty on out-of-range page', async () => { test('can getArticlesPaginated returns empty on out-of-range page', async () => {
const result = await getArticlesPaginated(999, 10); const result = await getArticlesPaginated(999, 10);
expect(result.data.length).toBe(0); expect(result.ok).toBe(true);
expect(result.page).toBe(999); if (!result.ok) return;
expect(result.total).toBeGreaterThanOrEqual(1); expect(result.value.data.length).toBe(0);
expect(result.value.page).toBe(999);
expect(result.value.total).toBeGreaterThanOrEqual(1);
}); });
test('can update article', async () => { test('can update article', async () => {
@@ -181,18 +213,23 @@ describe('ArticleService', () => {
description: 'Updated description', description: 'Updated description',
}; };
const article = await getArticleBySlug('test-article'); const slugResult = await getArticleBySlug('test-article');
expect(slugResult.ok).toBe(true);
if (!slugResult.ok) return;
const article = slugResult.value;
expect(article).toBeDefined(); expect(article).toBeDefined();
const updatedArticle = await updateArticle(article!.id, dataToUpdate); const result = await updateArticle(article!.id, dataToUpdate);
expect(updatedArticle).toBeDefined(); expect(result.ok).toBe(true);
expect(updatedArticle.id).toBe(article!.id); if (!result.ok) return;
expect(updatedArticle.title).toBe(dataToUpdate.title); expect(result.value).toBeDefined();
expect(updatedArticle.description).toBe(dataToUpdate.description); expect(result.value.id).toBe(article!.id);
expect(updatedArticle.slug).toBe(article!.slug); expect(result.value.title).toBe(dataToUpdate.title);
expect(updatedArticle.content).toBe(article!.content); expect(result.value.description).toBe(dataToUpdate.description);
expect(updatedArticle.externalId).toBeDefined(); expect(result.value.slug).toBe(article!.slug);
expect(result.value.content).toBe(article!.content);
expect(result.value.externalId).toBeDefined();
}); });
test('cannot update non-existing article', async () => { test('cannot update non-existing article', async () => {
@@ -200,9 +237,11 @@ describe('ArticleService', () => {
title: 'Updated Article Title', title: 'Updated Article Title',
}; };
await expect(updateArticle('9999', dataToUpdate)).rejects.toThrow( const result = await updateArticle('9999', dataToUpdate);
`Article with ID 9999 not found`
); expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toContain('9999');
}); });
test('can delete article', async () => { test('can delete article', async () => {
@@ -215,18 +254,25 @@ describe('ArticleService', () => {
authorId: authorId, authorId: authorId,
}; };
const savedArticle = await saveArticle(articleToSave); const saveResult = await saveArticle(articleToSave);
expect(savedArticle.id).toBeDefined(); expect(saveResult.ok).toBe(true);
if (!saveResult.ok) return;
expect(saveResult.value.id).toBeDefined();
await deleteArticle(savedArticle.id); const deleteResult = await deleteArticle(saveResult.value.id);
expect(deleteResult.ok).toBe(true);
const deletedArticle = await getArticleBySlug('article-to-delete'); const getResult = await getArticleBySlug('article-to-delete');
expect(deletedArticle).toBeNull(); expect(getResult.ok).toBe(true);
if (!getResult.ok) return;
expect(getResult.value).toBeNull();
}); });
test('cannot delete non-existing article', async () => { test('cannot delete non-existing article', async () => {
await expect(deleteArticle('9999')).rejects.toThrow( const result = await deleteArticle('9999');
`Article with ID 9999 not found`
); expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toContain('9999');
}); });
}); });

View File

@@ -35,14 +35,15 @@ describe('UserService', () => {
role: 'user', role: 'user',
}; };
const savedUser = await saveUser(userToSave); const result = await saveUser(userToSave);
expect(savedUser.id).toBeDefined(); expect(result.ok).toBe(true);
expect(savedUser.name).toBe(userToSave.name); if (!result.ok) return;
expect(savedUser.email).toBe(userToSave.email); expect(result.value.id).toBeDefined();
expect(savedUser.role).toBe(userToSave.role); expect(result.value.name).toBe(userToSave.name);
expect(savedUser.role).toBe(userToSave.role); expect(result.value.email).toBe(userToSave.email);
expect(savedUser.externalId).toBeDefined(); // Default to true if not set expect(result.value.role).toBe(userToSave.role);
expect(result.value.externalId).toBeDefined();
}); });
test('cannot save user with existing email', async () => { test('cannot save user with existing email', async () => {
@@ -51,23 +52,31 @@ describe('UserService', () => {
email: 'test@email.com', email: 'test@email.com',
role: 'user', role: 'user',
}; };
await expect(saveUser(userToSave)).rejects.toThrow(
`User with email ${userToSave.email} already exists` const result = await saveUser(userToSave);
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toContain(userToSave.email);
}); });
test('can getUserByEmail', async () => { test('can getUserByEmail', async () => {
const user = await getUserByEmail('test@email.com'); const result = await getUserByEmail('test@email.com');
expect(user).toBeDefined(); expect(result.ok).toBe(true);
expect(user?.email).toBe('test@email.com'); if (!result.ok) return;
expect(user?.name).toBe('Test User'); expect(result.value?.email).toBe('test@email.com');
expect(user?.role).toBe('user'); expect(result.value?.name).toBe('Test User');
expect(user?.externalId).toBeDefined(); // Default to true if not set expect(result.value?.role).toBe('user');
expect(result.value?.externalId).toBeDefined();
}); });
test('cannot getUserByEmail with non-existing email', async () => { test('cannot getUserByEmail with non-existing email', async () => {
await expect(getUserByEmail('missing@email.com')).resolves.toBeNull(); const result = await getUserByEmail('missing@email.com');
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.value).toBeNull();
}); });
test('can update user', async () => { test('can update user', async () => {
@@ -76,17 +85,21 @@ describe('UserService', () => {
role: 'admin', role: 'admin',
}; };
const user = await getUserByEmail('test@email.com'); const userResult = await getUserByEmail('test@email.com');
expect(userResult.ok).toBe(true);
if (!userResult.ok) return;
const user = userResult.value;
expect(user).toBeDefined(); expect(user).toBeDefined();
const updatedUser = await updateUser(user!.id, dataToUpdate); const result = await updateUser(user!.id, dataToUpdate);
expect(updatedUser).toBeDefined(); expect(result.ok).toBe(true);
expect(updatedUser.id).toBe(user!.id); if (!result.ok) return;
expect(updatedUser.name).toBe(dataToUpdate.name); expect(result.value.id).toBe(user!.id);
expect(updatedUser.role).toBe(dataToUpdate.role); expect(result.value.name).toBe(dataToUpdate.name);
expect(updatedUser.email).toBe(user!.email); expect(result.value.role).toBe(dataToUpdate.role);
expect(updatedUser.externalId).toBeDefined(); // Default to true if not set expect(result.value.email).toBe(user!.email);
expect(result.value.externalId).toBeDefined();
}); });
test('cannot update non-existing user', async () => { test('cannot update non-existing user', async () => {
@@ -95,9 +108,11 @@ describe('UserService', () => {
role: 'admin', role: 'admin',
}; };
await expect(updateUser('9999', dataToUpdate)).rejects.toThrow( const result = await updateUser('9999', dataToUpdate);
`User with ID 9999 not found`
); expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toContain('9999');
}); });
test('can sync admin user', async () => { test('can sync admin user', async () => {
@@ -131,12 +146,14 @@ describe('UserService', () => {
}, },
}, },
} as SessionClaims; } as SessionClaims;
const syncedUser = await syncUser(sessionClaims);
expect(syncedUser).toBeDefined(); const result = await syncUser(sessionClaims);
expect(syncedUser.name).toBe('Updated Name');
expect(syncedUser.email).toBe('test@email.com'); expect(result.ok).toBe(true);
expect(syncedUser.role).toBe('admin'); if (!result.ok) return;
expect(result.value.name).toBe('Updated Name');
expect(result.value.email).toBe('test@email.com');
expect(result.value.role).toBe('admin');
}); });
test('can sync internal user', async () => { test('can sync internal user', async () => {
@@ -170,12 +187,14 @@ describe('UserService', () => {
}, },
}, },
} as SessionClaims; } as SessionClaims;
const syncedUser = await syncUser(sessionClaims);
expect(syncedUser).toBeDefined(); const result = await syncUser(sessionClaims);
expect(syncedUser.name).toBe('Updated Name');
expect(syncedUser.email).toBe('test@email.com'); expect(result.ok).toBe(true);
expect(syncedUser.role).toBe('internal'); if (!result.ok) return;
expect(result.value.name).toBe('Updated Name');
expect(result.value.email).toBe('test@email.com');
expect(result.value.role).toBe('internal');
}); });
test('can sync user', async () => { test('can sync user', async () => {
@@ -202,12 +221,14 @@ describe('UserService', () => {
}, },
}, },
} as SessionClaims; } as SessionClaims;
const syncedUser = await syncUser(sessionClaims);
expect(syncedUser).toBeDefined(); const result = await syncUser(sessionClaims);
expect(syncedUser.name).toBe('Updated Name');
expect(syncedUser.email).toBe('test@email.com'); expect(result.ok).toBe(true);
expect(syncedUser.role).toBe('user'); if (!result.ok) return;
expect(result.value.name).toBe('Updated Name');
expect(result.value.email).toBe('test@email.com');
expect(result.value.role).toBe('user');
}); });
test('can sync saving new user', async () => { test('can sync saving new user', async () => {
@@ -234,11 +255,13 @@ describe('UserService', () => {
}, },
}, },
} as SessionClaims; } as SessionClaims;
const syncedUser = await syncUser(sessionClaims);
expect(syncedUser).toBeDefined(); const result = await syncUser(sessionClaims);
expect(syncedUser.name).toBe('Updated Name');
expect(syncedUser.email).toBe('new@email.com'); expect(result.ok).toBe(true);
expect(syncedUser.role).toBe('user'); if (!result.ok) return;
expect(result.value.name).toBe('Updated Name');
expect(result.value.email).toBe('new@email.com');
expect(result.value.role).toBe('user');
}); });
}); });