refactor: enhance error handling and state management in auth and image modules
This commit is contained in:
parent
c5fab7d61a
commit
13696e394f
@ -1,7 +1,12 @@
|
|||||||
import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel';
|
import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel';
|
||||||
import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel';
|
import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel';
|
||||||
import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||||
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
import {
|
||||||
|
StateFactory,
|
||||||
|
StatusType,
|
||||||
|
type AsyncState,
|
||||||
|
} from '$lib/common/adapter/presenter/asyncState';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export type AuthState = AsyncState<AuthViewModel>;
|
export type AuthState = AsyncState<AuthViewModel>;
|
||||||
@ -26,16 +31,18 @@ export class AuthBloc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadCurrentUser(): Promise<AuthState> {
|
private async loadCurrentUser(): Promise<AuthState> {
|
||||||
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
this.state.set(StateFactory.loading(get(this.state).data));
|
||||||
|
|
||||||
|
let result: AuthState;
|
||||||
|
try {
|
||||||
const user = await this.getCurrentUserUseCase.execute();
|
const user = await this.getCurrentUserUseCase.execute();
|
||||||
|
|
||||||
const userViewModel = user ? UserViewModel.fromEntity(user) : null;
|
const userViewModel = user ? UserViewModel.fromEntity(user) : null;
|
||||||
const authViewModel = AuthViewModel.fromEntity(userViewModel);
|
const authViewModel = AuthViewModel.fromEntity(userViewModel);
|
||||||
const result: AuthState = {
|
result = StateFactory.success(authViewModel);
|
||||||
status: StatusType.Success,
|
} catch (e) {
|
||||||
data: authViewModel,
|
result = StateFactory.error(e);
|
||||||
};
|
captureException(e);
|
||||||
|
}
|
||||||
|
|
||||||
this.state.set(result);
|
this.state.set(result);
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||||
import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto';
|
import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto';
|
||||||
|
import { HttpError } from '$lib/common/framework/web/httpError';
|
||||||
|
import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode';
|
||||||
import { Environment } from '$lib/environment';
|
import { Environment } from '$lib/environment';
|
||||||
|
|
||||||
export class AuthApiServiceImpl implements AuthApiService {
|
export class AuthApiServiceImpl implements AuthApiService {
|
||||||
@ -10,10 +12,14 @@ export class AuthApiServiceImpl implements AuthApiService {
|
|||||||
|
|
||||||
const response = await this.fetchFn(url);
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.status === HttpStatusCode.UNAUTHORIZED) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new HttpError(response.status, url);
|
||||||
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
return UserResponseDto.fromJson(json);
|
return UserResponseDto.fromJson(json);
|
||||||
}
|
}
|
||||||
|
@ -5,25 +5,56 @@ export enum StatusType {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IdleState<T> {
|
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
|
||||||
|
|
||||||
|
interface IdleState<T> {
|
||||||
status: StatusType.Idle;
|
status: StatusType.Idle;
|
||||||
data?: T;
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadingState<T> {
|
interface LoadingState<T> {
|
||||||
status: StatusType.Loading;
|
status: StatusType.Loading;
|
||||||
data?: T;
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuccessState<T> {
|
interface SuccessState<T> {
|
||||||
status: StatusType.Success;
|
status: StatusType.Success;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorState<T> {
|
interface ErrorState<T> {
|
||||||
status: StatusType.Error;
|
status: StatusType.Error;
|
||||||
data?: T;
|
data?: T;
|
||||||
error: Error;
|
error: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
|
export abstract class StateFactory {
|
||||||
|
static idle<T>(data?: T): AsyncState<T> {
|
||||||
|
return {
|
||||||
|
status: StatusType.Idle,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static loading<T>(data?: T): AsyncState<T> {
|
||||||
|
return {
|
||||||
|
status: StatusType.Loading,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static success<T>(data: T): AsyncState<T> {
|
||||||
|
return {
|
||||||
|
status: StatusType.Success,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static error<T>(error: unknown, data?: T): AsyncState<T> {
|
||||||
|
return {
|
||||||
|
status: StatusType.Error,
|
||||||
|
error: error instanceof Error ? error : new Error('Unknown error'),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
8
frontend/src/lib/common/framework/web/httpError.ts
Normal file
8
frontend/src/lib/common/framework/web/httpError.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly status: number,
|
||||||
|
url: URL
|
||||||
|
) {
|
||||||
|
super(`HTTP ${status} at ${url.href}`);
|
||||||
|
}
|
||||||
|
}
|
4
frontend/src/lib/common/framework/web/httpStatusCode.ts
Normal file
4
frontend/src/lib/common/framework/web/httpStatusCode.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum HttpStatusCode {
|
||||||
|
UNAUTHORIZED = 401,
|
||||||
|
NOT_FOUND = 404,
|
||||||
|
}
|
@ -1,6 +1,11 @@
|
|||||||
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
import {
|
||||||
|
StateFactory,
|
||||||
|
StatusType,
|
||||||
|
type AsyncState,
|
||||||
|
} from '$lib/common/adapter/presenter/asyncState';
|
||||||
import { ImageInfoViewModel } from '$lib/image/adapter/presenter/imageInfoViewModel';
|
import { ImageInfoViewModel } from '$lib/image/adapter/presenter/imageInfoViewModel';
|
||||||
import type { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase';
|
import type { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export type ImageInfoState = AsyncState<ImageInfoViewModel>;
|
export type ImageInfoState = AsyncState<ImageInfoViewModel>;
|
||||||
@ -25,15 +30,16 @@ export class ImageBloc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async uploadImage(file: File): Promise<ImageInfoState> {
|
private async uploadImage(file: File): Promise<ImageInfoState> {
|
||||||
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
this.state.set(StateFactory.loading(get(this.state).data));
|
||||||
|
|
||||||
let result: ImageInfoState;
|
let result: ImageInfoState;
|
||||||
try {
|
try {
|
||||||
const imageInfo = await this.uploadImageUseCase.execute(file);
|
const imageInfo = await this.uploadImageUseCase.execute(file);
|
||||||
const imageInfoViewModel = ImageInfoViewModel.fromEntity(imageInfo);
|
const imageInfoViewModel = ImageInfoViewModel.fromEntity(imageInfo);
|
||||||
result = { status: StatusType.Success, data: imageInfoViewModel };
|
result = StateFactory.success(imageInfoViewModel);
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
result = { status: StatusType.Error, error: error as Error };
|
result = StateFactory.error(e);
|
||||||
|
captureException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { HttpError } from '$lib/common/framework/web/httpError';
|
||||||
import { Environment } from '$lib/environment';
|
import { Environment } from '$lib/environment';
|
||||||
import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService';
|
import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService';
|
||||||
import { ImageInfoResponseDto } from '$lib/image/adapter/gateway/imageInfoResponseDto';
|
import { ImageInfoResponseDto } from '$lib/image/adapter/gateway/imageInfoResponseDto';
|
||||||
@ -17,8 +18,9 @@ export class ImageApiServiceImpl implements ImageApiService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`${response.status} ${response.statusText}`);
|
throw new HttpError(response.status, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return ImageInfoResponseDto.fromJson(data);
|
return ImageInfoResponseDto.fromJson(data);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
import {
|
||||||
|
StateFactory,
|
||||||
|
StatusType,
|
||||||
|
type AsyncState,
|
||||||
|
} from '$lib/common/adapter/presenter/asyncState';
|
||||||
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel';
|
||||||
import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export type PostState = AsyncState<PostViewModel>;
|
export type PostState = AsyncState<PostViewModel>;
|
||||||
@ -33,19 +38,22 @@ export class PostBloc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadPost(id: string): Promise<PostState> {
|
private async loadPost(id: string): Promise<PostState> {
|
||||||
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
this.state.set(StateFactory.loading(get(this.state).data));
|
||||||
|
|
||||||
|
let result: PostState;
|
||||||
|
try {
|
||||||
const post = await this.getPostUseCase.execute(id);
|
const post = await this.getPostUseCase.execute(id);
|
||||||
if (!post) {
|
if (!post) {
|
||||||
this.state.set({ status: StatusType.Error, error: new Error('Post not found') });
|
result = StateFactory.error(new Error('Post not found'));
|
||||||
return get(this.state);
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const postViewModel = PostViewModel.fromEntity(post);
|
const postViewModel = PostViewModel.fromEntity(post);
|
||||||
const result: PostState = {
|
result = StateFactory.success(postViewModel);
|
||||||
status: StatusType.Success,
|
} catch (e) {
|
||||||
data: postViewModel,
|
result = StateFactory.error(e);
|
||||||
};
|
captureException(e);
|
||||||
|
}
|
||||||
|
|
||||||
this.state.set(result);
|
this.state.set(result);
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
import {
|
||||||
|
StateFactory,
|
||||||
|
StatusType,
|
||||||
|
type AsyncState,
|
||||||
|
} from '$lib/common/adapter/presenter/asyncState';
|
||||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
import type { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
import type { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||||
|
import { captureException } from '@sentry/sveltekit';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export type PostListState = AsyncState<readonly PostInfoViewModel[]>;
|
export type PostListState = AsyncState<readonly PostInfoViewModel[]>;
|
||||||
@ -33,13 +38,17 @@ export class PostListBloc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadPosts(): Promise<PostListState> {
|
private async loadPosts(): Promise<PostListState> {
|
||||||
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
this.state.set(StateFactory.loading(get(this.state).data));
|
||||||
|
|
||||||
|
let result: PostListState;
|
||||||
|
try {
|
||||||
const posts = await this.getAllPostsUseCase.execute();
|
const posts = await this.getAllPostsUseCase.execute();
|
||||||
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
||||||
const result: PostListState = {
|
result = StateFactory.success(postViewModels);
|
||||||
status: StatusType.Success,
|
} catch (e) {
|
||||||
data: postViewModels,
|
result = StateFactory.error(e);
|
||||||
};
|
captureException(e);
|
||||||
|
}
|
||||||
|
|
||||||
this.state.set(result);
|
this.state.set(result);
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { HttpError } from '$lib/common/framework/web/httpError';
|
||||||
|
import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode';
|
||||||
import { Environment } from '$lib/environment';
|
import { Environment } from '$lib/environment';
|
||||||
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
||||||
@ -12,7 +14,7 @@ export class PostApiServiceImpl implements PostApiService {
|
|||||||
const response = await this.fetchFn(url);
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return [];
|
throw new HttpError(response.status, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
@ -24,10 +26,14 @@ export class PostApiServiceImpl implements PostApiService {
|
|||||||
|
|
||||||
const response = await this.fetchFn(url);
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (response.status === HttpStatusCode.NOT_FOUND) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new HttpError(response.status, url);
|
||||||
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
return PostResponseDto.fromJson(json);
|
return PostResponseDto.fromJson(json);
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,9 @@
|
|||||||
const isLoading = $derived(
|
const isLoading = $derived(
|
||||||
authState.status === StatusType.Loading || authState.status === StatusType.Idle
|
authState.status === StatusType.Loading || authState.status === StatusType.Idle
|
||||||
);
|
);
|
||||||
const hasError = $derived.by(() => {
|
const isAuthenticated = $derived(
|
||||||
if (authState.status === StatusType.Error) {
|
authState.status === StatusType.Success && authState.data.isAuthenticated
|
||||||
return true;
|
);
|
||||||
}
|
|
||||||
if (authState.status === StatusType.Success && !authState.data.isAuthenticated) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const links: DashboardLink[] = [
|
const links: DashboardLink[] = [
|
||||||
{ label: 'Post', href: '/dashboard/post' },
|
{ label: 'Post', href: '/dashboard/post' },
|
||||||
@ -39,7 +33,7 @@
|
|||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div></div>
|
<div></div>
|
||||||
{:else if hasError}
|
{:else if !isAuthenticated}
|
||||||
<ErrorPage />
|
<ErrorPage />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="min-h-content-height grid grid-cols-[auto_1fr]">
|
<div class="min-h-content-height grid grid-cols-[auto_1fr]">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user