aurelia/aurelia

View on GitHub
examples/realworld-conventions/src/state.ts

Summary

Maintainability
F
1 wk
Test Coverage
/* eslint-disable @typescript-eslint/no-empty-interface */
import { DI, ILifecycleHooks, ILogger } from 'aurelia';
import { IRouteableComponent, LoadInstruction, Navigation, Parameters, RoutingInstruction } from '@aurelia/router';
import { Article, ArticleListResponse, ArticleResponse, Comment, ErrorRecordResponse, IApiService, IJwtService, ArticleQueryParams, User, UserLogin, UserRegistration, UserResponse, UserUpdate, ArticleListQueryParams, Profile, ProfileResponse, ErrorList } from './api';

/**
 * Singleton `User` state that represents the currently logged-in user.
 */
export const IUserState = DI.createInterface<IUserState>('IUserState', x => x.singleton(UserState));
export interface IUserState extends UserState { }
export class UserState {
  errors: ErrorList = [];
  current = User.NONE;
  isAuth = false;

  constructor(
    @IJwtService private readonly jwt: IJwtService,
    @IApiService private readonly api: IApiService,
  ) { }

  async load() {
    if (this.jwt.isTokenValid()) {
      const resp = await this.api.getCurrentUser();

      this.setAuth(resp.user);
    } else {
      this.clearAuth();
    }
  }

  async login(login: UserLogin) {
    this.errors = [];
    const resp = await this.api.loginUser(login);
    if (resp instanceof ErrorRecordResponse) {
      this.clearAuth();
      this.errors = resp.toErrorList();
      return false;
    }
    this.setAuth(resp.user);
    return true;
  }

  logout(): void {
    this.clearAuth();
  }

  async register(registration: UserRegistration) {
    this.errors = [];
    const resp = await this.api.registerUser(registration);
    if (resp instanceof ErrorRecordResponse) {
      this.clearAuth();
      this.errors = resp.toErrorList();
      return false;
    }
    this.setAuth(resp.user);
    return true;
  }

  async update(user: UserUpdate) {
    const resp = await this.api.updateUser(user);

    if (resp instanceof UserResponse) {
      this.current = resp.user;
    }
  }

  private setAuth(user: User): void {
    this.jwt.saveToken(user.token);
    this.current = user;
    this.isAuth = true;
  }

  private clearAuth(): void {
    this.jwt.destroyToken();
    this.current = User.NONE;
    this.isAuth = false;
  }
}

/**
 * Singleton `Profile` state that represents the profile currently routed to.
 */
export const IProfileState = DI.createInterface<IProfileState>('IProfileState', x => x.singleton(ProfileState));
export interface IProfileState extends ProfileState { }
export class ProfileState {
  current = Profile.NONE;

  constructor(
    @IApiService private readonly api: IApiService,
  ) { }

  async toggleFollow() {
    const profile = this.current;
    const username = profile.username;
    let resp: ProfileResponse;
    if (profile.following) {
      resp = await this.api.unfollowUser(username);
    } else {
      resp = await this.api.followUser(username);
    }
    this.current = resp.profile;
  }

  async load(username: string) {
    const resp = await this.api.getProfile(username);
    this.current = resp.profile;
  }
}

/**
 * Singleton `Article` state that represents the article currently routed to.
 */
export const IArticleState = DI.createInterface<IArticleState>('IArticleState', x => x.singleton(ArticleState));
export interface IArticleState extends ArticleState { }
export class ArticleState {
  errors: ErrorList = [];
  current: Article = Article.NONE;
  comments: Comment[] = [];

  constructor(
    @IApiService private readonly api: IApiService,
  ) { }

  async toggleFollow() {
    const author = this.current.author;
    const username = author.username;
    let resp: ProfileResponse;
    if (author.following) {
      resp = await this.api.unfollowUser(username);
    } else {
      resp = await this.api.followUser(username);
    }
    this.current.author = resp.profile;
  }

  async toggleFavorite() {
    const article = this.current;
    let resp: ArticleResponse;
    if (article.favorited) {
      resp = await this.api.unfavoriteArticle(article.slug);
    } else {
      resp = await this.api.favoriteArticle(article.slug);
    }
    this.current = resp.article;
  }

  async load(slug = '') {
    this.current = Article.NONE;
    this.comments = [];

    if (slug) {
      const resp = await this.api.getArticle(slug);
      this.current = resp.article;
    }
  }

  async loadComments(slug: string) {
    const resp = await this.api.getCommentsFromArticle(slug);
    this.comments = resp.comments;
  }

  async save(article: Article) {
    this.errors = [];
    let resp: ErrorRecordResponse | ArticleResponse;
    if (article.slug) {
      resp = await this.api.updateArticle(article.slug, article);
    } else {
      resp = await this.api.createArticle(article);
    }
    if (resp instanceof ErrorRecordResponse) {
      this.errors = resp.toErrorList();
      return false;
    }
    this.current = resp.article
    return true;
  }

  async delete() {
    this.errors = [];
    await this.api.deleteArticle(this.current.slug);
    this.current = Article.NONE;
  }

  async addComment(body: string) {
    this.errors = [];
    const article = this.current;
    const resp = await this.api.addCommentToArticle(article.slug, { body });
    if (resp instanceof ErrorRecordResponse) {
      this.errors = resp.toErrorList();
      return false;
    }
    this.comments.push(resp.comment);
    return true;
  }

  async deleteComment(commentId: number) {
    this.errors = [];
    const article = this.current;
    await this.api.deleteCommentFromArticle(article.slug, commentId);
    const idx = this.comments.findIndex(x => x.id === commentId);
    this.comments.splice(idx, 1);
  }
}

/**
 * Singleton `Article` list state that represents the list of articles (and the query params that narrow them down) currently being viewed.
 */
export const IArticleListState = DI.createInterface<IArticleListState>('IArticleListState', x => x.singleton(ArticleListState));
export interface IArticleListState extends ArticleListState { }
export class ArticleListState {
  items: Article[] = [];
  itemsCount = 0;
  currentPage = 0;
  pages: number[] = [];
  params: ArticleQueryParams = ArticleListQueryParams.create({ limit: 20, offset: 0 });

  constructor(
    @IApiService private readonly api: IApiService,
  ) { }

  async toggleFavorite(slug: string) {
    const idx = this.items.findIndex(x => x.slug === slug);
    const article = this.items[idx];
    let resp: ArticleResponse;
    if (article.favorited) {
      resp = await this.api.unfavoriteArticle(slug);
    } else {
      resp = await this.api.favoriteArticle(slug);
    }
    this.items.splice(idx, 1, resp.article);
  }

  async load(params: ArticleQueryParams = this.params) {
    this.params = params;
    let resp: ArticleListResponse;
    if (params.type === 'all') {
      resp = await this.api.getArticles(params);
    } else {
      resp = await this.api.getFeedArticles(params);
    }
    this.items = resp.articles;
    this.itemsCount = resp.articlesCount;
    this.currentPage = (params.offset + this.items.length) / params.limit;
    this.pages = Array.from(Array(Math.ceil(resp.articlesCount / params.limit)), (_, i) => i + 1);
  }
}

/**
 * Singleton tags state that represents all global tags.
 */
export const ITagsState = DI.createInterface<ITagsState>('ITagsState', x => x.singleton(TagsState));
export interface ITagsState extends TagsState { }
export class TagsState {
  items: string[] = [];

  constructor(
    @IApiService private readonly api: IApiService,
  ) { }

  async load() {
    const resp = await this.api.getTags();
    this.items = resp.tags;
  }
}

export class AuthHandler implements ILifecycleHooks<IRouteableComponent, 'canLoad'> {
  constructor(
    @IUserState readonly auth: IUserState,
    @ILogger readonly logger: ILogger,
  ) {
    this.logger = logger.scopeTo('AuthHandler')
  }

  canLoad(vm: IRouteableComponent, _params: Parameters, _instruction: RoutingInstruction, navigation: Navigation): boolean | LoadInstruction | LoadInstruction[] | Promise<boolean | LoadInstruction | LoadInstruction[]> {
    if (!this.auth.isAuth) {
      this.logger.trace(`canLoad() - redirecting to login page`, navigation, this.auth);
      return 'login';
    }
    this.logger.trace(`canLoad() - proceeding`, navigation, this.auth);
    return true;
  }
}