feat: wrap article and user service functions with TypedResult for improved error handling
This commit is contained in:
@@ -70,7 +70,9 @@ const ArticleContentSkeleton = () => (
|
||||
|
||||
const ArticleContent = async ({ params }: ArticleContentProps) => {
|
||||
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();
|
||||
|
||||
|
||||
@@ -45,11 +45,9 @@ const ArticleList = async ({ searchParams }: ArticleListProps) => {
|
||||
const page = Math.max(1, Number(pageParam) || 1);
|
||||
const pageSize = Number(pageSizeParam) || PAGE_SIZE;
|
||||
|
||||
const {
|
||||
data: articles,
|
||||
totalPages,
|
||||
total,
|
||||
} = await getArticlesPaginated(page, pageSize);
|
||||
const paginationResult = await getArticlesPaginated(page, pageSize);
|
||||
if (!paginationResult.ok) throw paginationResult.error;
|
||||
const { data: articles, totalPages, total } = paginationResult.value;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -15,8 +15,12 @@ export async function GET() {
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
const syncedUser = await syncUser(parsedClaims);
|
||||
const syncResult = await syncUser(parsedClaims);
|
||||
if (!syncResult.ok) {
|
||||
console.error(syncResult.error);
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
await setSessionData('user', syncedUser);
|
||||
await setSessionData('user', syncResult.value);
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
@@ -8,28 +8,35 @@ 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 } from '@/utils/types/results';
|
||||
import { UUIDv4 } from '@/utils/types/uuid';
|
||||
|
||||
export const getArticleByExternalId = async (
|
||||
const _getArticleByExternalId = async (
|
||||
externalId: UUIDv4
|
||||
): 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
|
||||
): 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,
|
||||
pageSize: number = 10
|
||||
): 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
|
||||
): Promise<ArticleModel> => {
|
||||
const session = await getSessionData();
|
||||
@@ -38,10 +45,12 @@ export const saveArticle = async (
|
||||
}
|
||||
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,
|
||||
article: UpdateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
@@ -49,13 +58,45 @@ export const updateArticle = async (
|
||||
if (!session || !session?.user || session?.user.role !== 'admin') {
|
||||
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();
|
||||
if (!session || !session?.user || session?.user.role !== 'admin') {
|
||||
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);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PaginatedArticlesResult,
|
||||
UpdateArticleModel,
|
||||
} from '@/lib/feature/article/article.model';
|
||||
import { TypedResult, wrap } from '@/utils/types/results';
|
||||
import { UUIDv4 } from '@/utils/types/uuid';
|
||||
|
||||
export const articleEntityToModel = (
|
||||
@@ -25,11 +26,7 @@ export const articleEntityToModel = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Retrieves an artible by its external ID.
|
||||
* @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 (
|
||||
const _getArticleByExternalId = async (
|
||||
externalId: UUIDv4
|
||||
): Promise<ArticleModel | null> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
@@ -45,12 +42,7 @@ export const getArticleByExternalId = async (
|
||||
return articleEntityToModel(articleEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
const _getArticleBySlug = async (
|
||||
slug: string
|
||||
): Promise<ArticleModel | null> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
@@ -64,12 +56,7 @@ export const getArticleBySlug = async (
|
||||
return articleEntityToModel(articleEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
const _getArticlesByAuthorId = async (
|
||||
authorId: string
|
||||
): Promise<ArticleModel[]> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
@@ -79,13 +66,7 @@ export const getArticlesByAuthorId = async (
|
||||
return articleEntities.map(articleEntityToModel);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
const _getArticlesPaginated = async (
|
||||
page: number = 1,
|
||||
pageSize: number = 10
|
||||
): Promise<PaginatedArticlesResult> => {
|
||||
@@ -106,13 +87,7 @@ export const getArticlesPaginated = 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 (
|
||||
const _saveArticle = async (
|
||||
article: CreateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
@@ -129,14 +104,7 @@ export const saveArticle = async (
|
||||
return articleEntityToModel(await articleRepository.save(newArticle));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
const _updateArticle = async (
|
||||
articleId: string,
|
||||
article: UpdateArticleModel
|
||||
): Promise<ArticleModel> => {
|
||||
@@ -160,12 +128,7 @@ export const updateArticle = async (
|
||||
return articleEntityToModel(await articleRepository.save(existingArticle));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 _deleteArticle = async (articleId: string): Promise<void> => {
|
||||
const articleRepository = await getRepository(ArticleEntity);
|
||||
|
||||
const existingArticle = await articleRepository.findOneBy({
|
||||
@@ -177,3 +140,41 @@ export const deleteArticle = async (articleId: string): Promise<void> => {
|
||||
|
||||
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);
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
import { getRepository } from '@/lib/db/client';
|
||||
import { UserEntity } from '@/lib/db/entities';
|
||||
import { UserModel } from '@/lib/feature/user/user.model';
|
||||
import { userEntityToModel } from '@/lib/feature/user/user.service';
|
||||
import { getSessionData } from '@/lib/session/session-storage';
|
||||
import { TypedResult, wrap } from '@/utils/types/results';
|
||||
import { UUIDv4 } from '@/utils/types/uuid';
|
||||
|
||||
export const getUserByExternalId = async (externalId: UUIDv4) => {
|
||||
const _getUserByExternalId = async (
|
||||
externalId: UUIDv4
|
||||
): Promise<UserModel | null> => {
|
||||
const sessionData = await getSessionData();
|
||||
if (
|
||||
!sessionData ||
|
||||
@@ -28,3 +32,7 @@ export const getUserByExternalId = async (externalId: UUIDv4) => {
|
||||
|
||||
return userEntityToModel(userEntity);
|
||||
};
|
||||
|
||||
export const getUserByExternalId: (
|
||||
externalId: UUIDv4
|
||||
) => Promise<TypedResult<UserModel | null>> = wrap(_getUserByExternalId);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
UpdateUserModel,
|
||||
UserModel,
|
||||
} from '@/lib/feature/user/user.model';
|
||||
import { TypedResult, wrap } from '@/utils/types/results';
|
||||
|
||||
export const userEntityToModel = (userEntity: UserEntity): UserModel => {
|
||||
return {
|
||||
@@ -17,14 +18,7 @@ export const userEntityToModel = (userEntity: UserEntity): UserModel => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 _getUserByEmail = async (email: string): Promise<UserModel | null> => {
|
||||
const userRepository = await getRepository(UserEntity);
|
||||
|
||||
const userEntity = await userRepository.findOneBy({ email });
|
||||
@@ -36,13 +30,7 @@ export const getUserByEmail = async (
|
||||
return userEntityToModel(userEntity);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 _saveUser = async (user: CreateUserModel): Promise<UserModel> => {
|
||||
const userRepository = await getRepository(UserEntity);
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
const _updateUser = async (
|
||||
userId: string,
|
||||
user: UpdateUserModel
|
||||
): Promise<UserModel> => {
|
||||
@@ -78,33 +59,55 @@ export const updateUser = async (
|
||||
return userEntityToModel(await userRepository.save(existingUser));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 _syncUser = async (sessionClaims: SessionClaims): Promise<UserModel> => {
|
||||
const { full_name, email } = sessionClaims.user;
|
||||
const role = sessionClaims.user.public_metadata.role;
|
||||
|
||||
const existingUser = await getUserByEmail(email);
|
||||
const existingUser = await _getUserByEmail(email);
|
||||
if (!existingUser) {
|
||||
return await saveUser({
|
||||
return await _saveUser({
|
||||
name: full_name,
|
||||
email: email,
|
||||
role: role,
|
||||
});
|
||||
}
|
||||
|
||||
return await updateUser(existingUser.id, {
|
||||
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<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 };
|
||||
|
||||
@@ -101,23 +101,20 @@ export const CreateArticleForm = () => {
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
async (data: z.infer<typeof formSchema>) => {
|
||||
try {
|
||||
const result = await saveArticle({ ...data });
|
||||
toast.success('Article created successfully!', {
|
||||
description: `Article "${result.title}" has been created.`,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
form.reset();
|
||||
resetFiles();
|
||||
} catch (error) {
|
||||
const result = await saveArticle({ ...data });
|
||||
if (!result.ok) {
|
||||
toast.error('Failed to create article', {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred',
|
||||
description: result.error.message,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success('Article created successfully!', {
|
||||
description: `Article "${result.value.title}" has been created.`,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
form.reset();
|
||||
resetFiles();
|
||||
},
|
||||
[form, resetFiles]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user