From 3ea1112369dacbbf1725df7aadf6caa1f4be225a Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sat, 11 Apr 2026 02:01:12 -0300 Subject: [PATCH] feat: refactor article and user service functions to use TypedResult for enhanced error handling --- src/lib/feature/article/article.external.ts | 134 ++++++----- src/lib/feature/article/article.service.ts | 234 ++++++++++---------- src/lib/feature/user/user.external.ts | 50 ++--- src/lib/feature/user/user.service.ts | 138 ++++++------ 4 files changed, 274 insertions(+), 282 deletions(-) diff --git a/src/lib/feature/article/article.external.ts b/src/lib/feature/article/article.external.ts index 12f02f7..fb377f6 100644 --- a/src/lib/feature/article/article.external.ts +++ b/src/lib/feature/article/article.external.ts @@ -11,92 +11,88 @@ import { getSessionData } from '@/lib/session/session-storage'; import { TypedResult, wrap } from '@/utils/types/results'; import { UUIDv4 } from '@/utils/types/uuid'; -const _getArticleByExternalId = async ( - externalId: UUIDv4 -): Promise => { - const result = await service.getArticleByExternalId(externalId); - if (!result.ok) throw result.error; - return result.value; -}; - -const _getArticleBySlug = async ( - slug: string -): Promise => { - const result = await service.getArticleBySlug(slug); - if (!result.ok) throw result.error; - return result.value; -}; - -const _getArticlesPaginated = async ( - page: number = 1, - pageSize: number = 10 -): Promise => { - const result = await service.getArticlesPaginated(page, pageSize); - if (!result.ok) throw result.error; - return result.value; -}; - -const _saveArticle = async ( - article: CreateArticleModel -): Promise => { - const session = await getSessionData(); - if (!session || !session?.user || session?.user.role !== 'admin') { - throw new Error('Unauthorized: Only admin users can save articles.'); - } - article.authorId = session.user.id; - - const result = await service.saveArticle(article); - if (!result.ok) throw result.error; - return result.value; -}; - -const _updateArticle = async ( - articleId: string, - article: UpdateArticleModel -): Promise => { - const session = await getSessionData(); - if (!session || !session?.user || session?.user.role !== 'admin') { - throw new Error('Unauthorized: Only admin users can save articles.'); - } - - const result = await service.updateArticle(articleId, article); - if (!result.ok) throw result.error; - return result.value; -}; - -const _deleteArticle = async (articleId: string): Promise => { - const session = await getSessionData(); - if (!session || !session?.user || session?.user.role !== 'admin') { - throw new Error('Unauthorized: Only admin users can delete articles.'); - } - - const result = await service.deleteArticle(articleId); - if (!result.ok) throw result.error; -}; - export const getArticleByExternalId: ( externalId: UUIDv4 -) => Promise> = wrap(_getArticleByExternalId); +) => Promise> = wrap( + async (externalId: UUIDv4): Promise => { + const result = await service.getArticleByExternalId(externalId); + if (!result.ok) throw result.error; + return result.value; + } +); export const getArticleBySlug: ( slug: string -) => Promise> = wrap(_getArticleBySlug); +) => Promise> = wrap( + async (slug: string): Promise => { + const result = await service.getArticleBySlug(slug); + if (!result.ok) throw result.error; + return result.value; + } +); export const getArticlesPaginated: ( page?: number, pageSize?: number ) => Promise> = wrap( - _getArticlesPaginated + async ( + page: number = 1, + pageSize: number = 10 + ): Promise => { + const result = await service.getArticlesPaginated(page, pageSize); + if (!result.ok) throw result.error; + return result.value; + } ); export const saveArticle: ( article: CreateArticleModel -) => Promise> = wrap(_saveArticle); +) => Promise> = wrap( + async (article: CreateArticleModel): Promise => { + const session = await getSessionData(); + if (!session || !session?.user || session?.user.role !== 'admin') { + throw new Error( + 'Unauthorized: Only admin users can save articles.' + ); + } + article.authorId = session.user.id; + + const result = await service.saveArticle(article); + if (!result.ok) throw result.error; + return result.value; + } +); export const updateArticle: ( articleId: string, article: UpdateArticleModel -) => Promise> = wrap(_updateArticle); +) => Promise> = wrap( + async ( + articleId: string, + article: UpdateArticleModel + ): Promise => { + const session = await getSessionData(); + if (!session || !session?.user || session?.user.role !== 'admin') { + throw new Error( + 'Unauthorized: Only admin users can save articles.' + ); + } + + const result = await service.updateArticle(articleId, article); + if (!result.ok) throw result.error; + return result.value; + } +); export const deleteArticle: (articleId: string) => Promise> = - wrap(_deleteArticle); + wrap(async (articleId: string): Promise => { + const session = await getSessionData(); + if (!session || !session?.user || session?.user.role !== 'admin') { + throw new Error( + 'Unauthorized: Only admin users can delete articles.' + ); + } + + const result = await service.deleteArticle(articleId); + if (!result.ok) throw result.error; + }); diff --git a/src/lib/feature/article/article.service.ts b/src/lib/feature/article/article.service.ts index 4b26b21..6eaa5ae 100644 --- a/src/lib/feature/article/article.service.ts +++ b/src/lib/feature/article/article.service.ts @@ -26,155 +26,145 @@ export const articleEntityToModel = ( }; }; -const _getArticleByExternalId = async ( - externalId: UUIDv4 -): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - const articleEntity = await articleRepository.findOneBy({ - externalId: externalId, - }); - - if (!articleEntity) { - return null; - } - - return articleEntityToModel(articleEntity); -}; - -const _getArticleBySlug = async ( - slug: string -): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - const articleEntity = await articleRepository.findOneBy({ slug }); - - if (!articleEntity) { - return null; - } - - return articleEntityToModel(articleEntity); -}; - -const _getArticlesByAuthorId = async ( - authorId: string -): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - const articleEntities = await articleRepository.findBy({ authorId }); - - return articleEntities.map(articleEntityToModel); -}; - -const _getArticlesPaginated = async ( - page: number = 1, - pageSize: number = 10 -): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - const [articleEntities, total] = await articleRepository.findAndCount({ - order: { createdAt: 'DESC' }, - skip: (page - 1) * pageSize, - take: pageSize, - }); - - return { - data: articleEntities.map(articleEntityToModel), - total, - page, - pageSize, - totalPages: Math.ceil(total / pageSize), - }; -}; - -const _saveArticle = async ( - article: CreateArticleModel -): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - if (!article.authorId) { - throw new Error('Author ID is required to save an article'); - } - - if (!!(await articleRepository.findOneBy({ slug: article.slug }))) { - throw new Error(`Article with slug ${article.slug} already exists`); - } - - const newArticle = articleRepository.create(article); - return articleEntityToModel(await articleRepository.save(newArticle)); -}; - -const _updateArticle = async ( - articleId: string, - article: UpdateArticleModel -): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - const existingArticle = await articleRepository.findOneBy({ - id: articleId, - }); - if (!existingArticle) { - throw new Error(`Article with ID ${articleId} not found`); - } - - if (!!article.title) existingArticle.title = article.title; - if (!!article.slug) existingArticle.slug = article.slug; - if (!!article.description) - existingArticle.description = article.description; - if (!!article.coverImageUrl) - existingArticle.coverImageUrl = article.coverImageUrl; - if (!!article.content) existingArticle.content = article.content; - - return articleEntityToModel(await articleRepository.save(existingArticle)); -}; - -const _deleteArticle = async (articleId: string): Promise => { - const articleRepository = await getRepository(ArticleEntity); - - const existingArticle = await articleRepository.findOneBy({ - id: articleId, - }); - if (!existingArticle) { - throw new Error(`Article with ID ${articleId} not found`); - } - - await articleRepository.remove(existingArticle); -}; - /** Retrieves an article by its external ID. */ export const getArticleByExternalId: ( externalId: UUIDv4 -) => Promise> = wrap(_getArticleByExternalId); +) => Promise> = wrap( + async (externalId: UUIDv4): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + const articleEntity = await articleRepository.findOneBy({ + externalId: externalId, + }); + + if (!articleEntity) { + return null; + } + + return articleEntityToModel(articleEntity); + } +); /** Retrieves an article by its slug. */ export const getArticleBySlug: ( slug: string -) => Promise> = wrap(_getArticleBySlug); +) => Promise> = wrap( + async (slug: string): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + const articleEntity = await articleRepository.findOneBy({ slug }); + + if (!articleEntity) { + return null; + } + + return articleEntityToModel(articleEntity); + } +); /** Retrieves all articles by a given author ID. */ export const getArticlesByAuthorId: ( authorId: string -) => Promise> = wrap(_getArticlesByAuthorId); +) => Promise> = wrap( + async (authorId: string): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + const articleEntities = await articleRepository.findBy({ authorId }); + + return articleEntities.map(articleEntityToModel); + } +); /** Retrieves a paginated list of articles ordered by creation date descending. */ export const getArticlesPaginated: ( page?: number, pageSize?: number ) => Promise> = wrap( - _getArticlesPaginated + async ( + page: number = 1, + pageSize: number = 10 + ): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + const [articleEntities, total] = await articleRepository.findAndCount({ + order: { createdAt: 'DESC' }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + + return { + data: articleEntities.map(articleEntityToModel), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize), + }; + } ); /** Saves a new article to the database. */ export const saveArticle: ( article: CreateArticleModel -) => Promise> = wrap(_saveArticle); +) => Promise> = wrap( + async (article: CreateArticleModel): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + if (!article.authorId) { + throw new Error('Author ID is required to save an article'); + } + + if (!!(await articleRepository.findOneBy({ slug: article.slug }))) { + throw new Error(`Article with slug ${article.slug} already exists`); + } + + const newArticle = articleRepository.create(article); + return articleEntityToModel(await articleRepository.save(newArticle)); + } +); /** Updates an existing article in the database. */ export const updateArticle: ( articleId: string, article: UpdateArticleModel -) => Promise> = wrap(_updateArticle); +) => Promise> = wrap( + async ( + articleId: string, + article: UpdateArticleModel + ): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + const existingArticle = await articleRepository.findOneBy({ + id: articleId, + }); + if (!existingArticle) { + throw new Error(`Article with ID ${articleId} not found`); + } + + if (!!article.title) existingArticle.title = article.title; + if (!!article.slug) existingArticle.slug = article.slug; + if (!!article.description) + existingArticle.description = article.description; + if (!!article.coverImageUrl) + existingArticle.coverImageUrl = article.coverImageUrl; + if (!!article.content) existingArticle.content = article.content; + + return articleEntityToModel( + await articleRepository.save(existingArticle) + ); + } +); /** Deletes an article from the database. */ export const deleteArticle: (articleId: string) => Promise> = - wrap(_deleteArticle); + wrap(async (articleId: string): Promise => { + const articleRepository = await getRepository(ArticleEntity); + + const existingArticle = await articleRepository.findOneBy({ + id: articleId, + }); + if (!existingArticle) { + throw new Error(`Article with ID ${articleId} not found`); + } + + await articleRepository.remove(existingArticle); + }); diff --git a/src/lib/feature/user/user.external.ts b/src/lib/feature/user/user.external.ts index c20b606..252af65 100644 --- a/src/lib/feature/user/user.external.ts +++ b/src/lib/feature/user/user.external.ts @@ -8,31 +8,29 @@ import { getSessionData } from '@/lib/session/session-storage'; import { TypedResult, wrap } from '@/utils/types/results'; import { UUIDv4 } from '@/utils/types/uuid'; -const _getUserByExternalId = async ( - externalId: UUIDv4 -): Promise => { - const sessionData = await getSessionData(); - if ( - !sessionData || - !sessionData.user || - sessionData.user.externalId !== externalId - ) { - throw new Error('User not permitted to access this data'); - } - - const userRepository = await getRepository(UserEntity); - - const userEntity = await userRepository.findOne({ - where: { externalId: externalId }, - }); - - if (!userEntity) { - return null; - } - - return userEntityToModel(userEntity); -}; - export const getUserByExternalId: ( externalId: UUIDv4 -) => Promise> = wrap(_getUserByExternalId); +) => Promise> = wrap( + async (externalId: UUIDv4): Promise => { + const sessionData = await getSessionData(); + if ( + !sessionData || + !sessionData.user || + sessionData.user.externalId !== externalId + ) { + throw new Error('User not permitted to access this data'); + } + + const userRepository = await getRepository(UserEntity); + + const userEntity = await userRepository.findOne({ + where: { externalId: externalId }, + }); + + if (!userEntity) { + return null; + } + + return userEntityToModel(userEntity); + } +); diff --git a/src/lib/feature/user/user.service.ts b/src/lib/feature/user/user.service.ts index 6e0fc73..968a231 100644 --- a/src/lib/feature/user/user.service.ts +++ b/src/lib/feature/user/user.service.ts @@ -18,80 +18,42 @@ export const userEntityToModel = (userEntity: UserEntity): UserModel => { }; }; -const _getUserByEmail = async (email: string): Promise => { - const userRepository = await getRepository(UserEntity); - - const userEntity = await userRepository.findOneBy({ email }); - - if (!userEntity) { - return null; - } - - return userEntityToModel(userEntity); -}; - -const _saveUser = async (user: CreateUserModel): Promise => { - const userRepository = await getRepository(UserEntity); - - if (!!(await userRepository.findOneBy({ email: user.email }))) { - throw new Error(`User with email ${user.email} already exists`); - } - - const newUser = userRepository.create(user); - return userEntityToModel(await userRepository.save(newUser)); -}; - -const _updateUser = async ( - userId: string, - user: UpdateUserModel -): Promise => { - const userRepository = await getRepository(UserEntity); - - const existingUser = await userRepository.findOneBy({ id: userId }); - if (!existingUser) { - throw new Error(`User with ID ${userId} not found`); - } - - if (!!user.email) existingUser.email = user.email; - if (!!user.name) existingUser.name = user.name; - if (!!user.role) existingUser.role = user.role; - - return userEntityToModel(await userRepository.save(existingUser)); -}; - -const _syncUser = async (sessionClaims: SessionClaims): Promise => { - const { full_name, email } = sessionClaims.user; - const role = sessionClaims.user.public_metadata.role; - - const existingUser = await _getUserByEmail(email); - if (!existingUser) { - return await _saveUser({ - name: full_name, - email: email, - role: role, - }); - } - - return await _updateUser(existingUser.id, { - name: full_name, - email: existingUser.email, - role: role, - }); -}; - /** * Retrieves a user by their email address. * @param email - The email address of the user to retrieve. * @returns {Promise>} The user model if found, otherwise null. */ -export const getUserByEmail = wrap(_getUserByEmail); +export const getUserByEmail = wrap( + async (email: string): Promise => { + const userRepository = await getRepository(UserEntity); + + const userEntity = await userRepository.findOneBy({ email }); + + if (!userEntity) { + return null; + } + + return userEntityToModel(userEntity); + } +); /** * Saves a new user to the database. * @param user - The user data to save. * @returns {Promise>} The saved user model. */ -export const saveUser = wrap(_saveUser); +export const saveUser = wrap( + async (user: CreateUserModel): Promise => { + const userRepository = await getRepository(UserEntity); + + if (!!(await userRepository.findOneBy({ email: user.email }))) { + throw new Error(`User with email ${user.email} already exists`); + } + + const newUser = userRepository.create(user); + return userEntityToModel(await userRepository.save(newUser)); + } +); /** * Updates an existing user in the database. @@ -99,7 +61,22 @@ export const saveUser = wrap(_saveUser); * @param user - The new user data. * @returns {Promise>} The updated user model. */ -export const updateUser = wrap(_updateUser); +export const updateUser = wrap( + async (userId: string, user: UpdateUserModel): Promise => { + const userRepository = await getRepository(UserEntity); + + const existingUser = await userRepository.findOneBy({ id: userId }); + if (!existingUser) { + throw new Error(`User with ID ${userId} not found`); + } + + if (!!user.email) existingUser.email = user.email; + if (!!user.name) existingUser.name = user.name; + if (!!user.role) existingUser.role = user.role; + + return userEntityToModel(await userRepository.save(existingUser)); + } +); /** * Synchronizes a user with the database. @@ -107,7 +84,38 @@ export const updateUser = wrap(_updateUser); * @param sessionClaims Session Claims from the Auth Provider * @returns {Promise>} The synchronized user model. */ -export const syncUser = wrap(_syncUser); +export const syncUser = wrap( + async (sessionClaims: SessionClaims): Promise => { + const { full_name, email } = sessionClaims.user; + const role = sessionClaims.user.public_metadata.role; + + const existingUserResult = await getUserByEmail(email); + if (!existingUserResult.ok || !existingUserResult.value) { + const saveResult = await saveUser({ + name: full_name, + email: email, + role: role, + }); + if (!saveResult.ok) { + throw new Error(`User with email ${email} already exists`); + } + return saveResult.value; + } + const existingUser = existingUserResult.value; + + const updateResult = await updateUser(existingUser.id, { + name: full_name, + email: existingUser.email, + role: role, + }); + if (!updateResult.ok) { + throw new Error( + `Failed to update user with email ${email}: ${updateResult.error}` + ); + } + return updateResult.value; + } +); // Explicit re-export for TypeScript consumers who need the result type export type { TypedResult };