jelhan/croodle

View on GitHub
app/models/poll.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import Option from './option';
import User from './user';
import { TrackedArray } from 'tracked-built-ins';
import { NotFoundError, apiUrl } from '../utils/api';
import { decrypt, encrypt } from '../utils/encryption';
import answersForAnswerType from '../utils/answers-for-answer-type';
import fetch from 'fetch';
import config from 'croodle/config/environment';
import type { SelectionInput } from './selection';

const DAY_STRING_LENGTH = 10; // 'YYYY-MM-DD'.length

export type AnswerType = 'YesNo' | 'YesNoMaybe' | 'FreeText';
type OptionInput = { title: string }[];
export type PollType = 'FindADate' | 'MakeAPoll';
type PollInput = {
  anonymousUser: boolean;
  answerType: AnswerType;
  creationDate: string;
  description: string;
  expirationDate: string;
  forceAnswer: boolean;
  id: string;
  options: OptionInput;
  pollType: PollType;
  timezone: string | null;
  title: string;
  users: User[];
  version: string;
};

export default class Poll {
  // Is participation without user name possibile?
  anonymousUser: boolean;

  // YesNo, YesNoMaybe or Freetext
  answerType: AnswerType;

  // ISO-8601 combined date and time string in UTC
  creationDate: string;

  // poll's description
  description: string;

  // ISO 8601 date + time string in UTC
  expirationDate: string;

  // Must all options been answered?
  forceAnswer: boolean;

  // ID of the poll
  id: string;

  // array of poll's options
  options: Option[];

  // FindADate or MakeAPoll
  pollType: 'FindADate' | 'MakeAPoll';

  // timezone poll got created in (like "Europe/Berlin")
  timezone: string | null;

  // polls title
  title: string;

  // participants of the poll
  users: TrackedArray<User>;

  // Croodle version poll got created with
  version: string;

  get answers() {
    const { answerType } = this;
    return answersForAnswerType(answerType);
  }

  get hasTimes() {
    if (this.isMakeAPoll) {
      return false;
    }

    return this.options.some((option) => {
      return option.title.length > DAY_STRING_LENGTH;
    });
  }

  get isFindADate() {
    return this.pollType === 'FindADate';
  }

  get isFreeText() {
    return this.answerType === 'FreeText';
  }

  get isMakeAPoll() {
    return this.pollType === 'MakeAPoll';
  }

  constructor({
    anonymousUser,
    answerType,
    creationDate,
    description,
    expirationDate,
    forceAnswer,
    id,
    options,
    pollType,
    timezone,
    title,
    users,
    version,
  }: PollInput) {
    this.anonymousUser = anonymousUser;
    this.answerType = answerType;
    this.creationDate = creationDate;
    this.description = description;
    this.expirationDate = expirationDate;
    this.forceAnswer = forceAnswer;
    this.id = id;
    this.options = options.map((option) => new Option(option));
    this.pollType = pollType;
    this.timezone = timezone;
    this.title = title;
    this.users = new TrackedArray(users);
    this.version = version;
  }

  static async load(id: string, passphrase: string) {
    const url = apiUrl(`polls/${id}`);

    // TODO: Handle network connectivity error
    const response = await fetch(url);

    if (!response.ok) {
      if (response.status === 404) {
        throw new NotFoundError(
          `A poll with ID ${id} could not be found at the server.`,
        );
      } else {
        throw new Error(
          `Unexpected server-side error. Server responsed with ${response.status} (${response.statusText})`,
        );
      }
    }

    // TODO: Handle malformed server response
    const payload = (await response.json()) as {
      poll: {
        anonymousUser: string;
        answerType: string;
        creationDate: string;
        description: string;
        expirationDate: string;
        forceAnswer: string;
        id: string;
        options: string;
        pollType: string;
        timezone: string;
        title: string;
        users: {
          creationDate: string;
          id: string;
          name: string;
          selections: string;
          version: string;
        }[];
        version: string;
      };
    };

    return new Poll({
      anonymousUser: decrypt(payload.poll.anonymousUser, passphrase) as boolean,
      answerType: decrypt(payload.poll.answerType, passphrase) as AnswerType,
      creationDate: decrypt(payload.poll.creationDate, passphrase) as string,
      description: decrypt(payload.poll.description, passphrase) as string,
      expirationDate: decrypt(
        payload.poll.expirationDate,
        passphrase,
      ) as string,
      forceAnswer: decrypt(payload.poll.forceAnswer, passphrase) as boolean,
      id: payload.poll.id,
      options: decrypt(payload.poll.options, passphrase) as OptionInput,
      pollType: decrypt(payload.poll.pollType, passphrase) as PollType,
      timezone: decrypt(payload.poll.timezone, passphrase) as string | null,
      title: decrypt(payload.poll.title, passphrase) as string,
      users: payload.poll.users.map((user) => {
        return new User({
          creationDate: decrypt(user.creationDate, passphrase) as string,
          id: user.id,
          name: decrypt(user.name, passphrase) as string,
          selections: decrypt(user.selections, passphrase) as SelectionInput[],
          version: user.version,
        });
      }),
      version: payload.poll.version,
    });
  }

  static async create(
    {
      anonymousUser,
      answerType,
      description,
      expirationDate,
      forceAnswer,
      options,
      pollType,
      title,
    }: {
      anonymousUser: boolean;
      answerType: AnswerType;
      description: string;
      expirationDate: string;
      forceAnswer: boolean;
      options: OptionInput;
      pollType: PollType;
      title: string;
    },
    passphrase: string,
  ) {
    const creationDate = new Date().toISOString();
    const version = config.APP['version'] as string;
    const timezone =
      pollType === 'FindADate' &&
      options.some(({ title }) => {
        return title.length >= 'YYYY-MM-DDTHH:mm'.length;
      })
        ? Intl.DateTimeFormat().resolvedOptions().timeZone
        : null;

    const payload = {
      poll: {
        anonymousUser: encrypt(anonymousUser, passphrase),
        answerType: encrypt(answerType, passphrase),
        creationDate: encrypt(creationDate, passphrase),
        description: encrypt(description, passphrase),
        expirationDate: encrypt(expirationDate, passphrase),
        forceAnswer: encrypt(forceAnswer, passphrase),
        options: encrypt(options, passphrase),
        pollType: encrypt(pollType, passphrase),
        serverExpirationDate: expirationDate,
        timezone: encrypt(timezone, passphrase),
        title: encrypt(title, passphrase),
        version,
      },
    };

    // TODO: Handle network connectivity issue
    const response = await fetch(apiUrl('polls'), {
      body: JSON.stringify(payload),
      method: 'POST',
    });

    if (!response.ok) {
      throw new Error(
        `Unexpected server-side error. Server responded with ${response.status} (${response.statusText})`,
      );
    }
    const responseDocument = await response.json();
    const { id } = responseDocument.poll;

    return new Poll({
      anonymousUser,
      answerType,
      creationDate,
      description,
      expirationDate,
      forceAnswer,
      id,
      options,
      pollType,
      timezone,
      title,
      users: [],
      version,
    });
  }
}