open-learning-exchange/planet

View on GitHub
src/app/validators/validator.service.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { Injectable } from '@angular/core';
import { AbstractControl, ValidationErrors } from '@angular/forms';
// Make sure not to import the entire rxjs library!!!
import { Observable, timer, of } from 'rxjs';

import { findDocuments } from '../shared/mangoQueries';
import { CouchService } from '../shared/couchdb.service';
import { UserService } from '../shared/user.service';
import { switchMap, map, catchError } from 'rxjs/operators';
import { debug } from '../debug-operator';
import { deepEqual } from '../shared/utils';

@Injectable({
  providedIn: 'root'
})
export class ValidatorService {

  constructor(
    private couchService: CouchService,
    private userService: UserService
  ) {}

  public checkUnique$(db: string, selectors: any, opts = {}): Observable<boolean> {
    return this.couchService
      .post(`${db}/_find`, findDocuments(selectors, [ '_id' ], 0, 1), opts)
      .pipe(map(data => {
        return data.docs.length > 0;
      }));
  }

  public isUnique$(
    dbName: string,
    fieldName: string,
    ac: AbstractControl,
    { exceptions = [], opts = {}, selectors = {}, errorType = 'duplicate' } = { exceptions: [], opts: {}, selectors: {} }
  ): Observable<ValidationErrors | null> {
    if (exceptions.findIndex(exception => ac.value === exception) > -1) {
      return of(null);
    }
    selectors = { [fieldName]: { '$regex': `(?i)^\\s*${this.replaceSpecialChar(ac.value.trim())}\\s*$` }, ...selectors };
    // calls service every .5s for input change
    return timer(500).pipe(
      switchMap(() => this.checkUnique$(dbName, selectors, opts)),
      map(exists => {
        if (exists) {
          return { [errorType]: true };
        }
        return null;
      })
    ).pipe(debug('Checking uniqueness of ' + fieldName + ' in ' + dbName));
  }

  public checkUniqueResourceTitle$(ac: AbstractControl, id: string, privateFor: any = null) {
    return timer(500).pipe(
      switchMap(
        () => this.couchService.get(`resources/_design/resources/_view/titles?key="${encodeURIComponent(ac.value.toLowerCase().trim())}"`)
      ),
      map(({ rows }) => {
        if (
          rows.length === 0 ||
          rows.every(({ id: rowId }) => rowId === id) ||
          rows.every(({ value }) => value && !deepEqual(value, privateFor))
        ) {
          return null;
        }
        return { duplicate: true };
      })
    );
  }

  /**
   * Used in isUnique() to replace special characters inputted in fields
   * @param fieldInput inputted field values
   */
  private replaceSpecialChar(fieldInput) {
    return fieldInput.replace(/[-.*+?^${}()|[\]\\]/g, '\\$&');
  }

  public checkPassword$(ac: AbstractControl): Observable<boolean> {
    return this.couchService.post('_session', { 'name': this.userService.get().name, 'password': ac.value }, { withCredentials: false })
    .pipe(
      map(data => {
        return null;
      }),
      catchError(err => {
        return of({ invalidPassword: true });
      }));
  }

  private roundTimestamp(timestamp: number) {
    return new Date(timestamp).setHours(0, 0, 0, 0);
  }

  public notDateInFuture$(ac: AbstractControl): Observable<ValidationErrors | null> {
    return this.couchService.currentTime().pipe(map(date => ac.value > date ? ({ invalidFutureDate: true }) : null));
  }

  public notDateInPast$(ac: AbstractControl): Observable<ValidationErrors | null> {
    return (this.couchService.currentTime() as Observable<number>).pipe(
      map(date => ac.value < this.roundTimestamp(date) ? ({ dateInPast: true }) : null)
    );
  }

}