open-learning-exchange/planet

View on GitHub
src/app/manager-dashboard/sync.directive.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { Directive, HostListener, Output, EventEmitter } from '@angular/core';
import { CouchService } from '../shared/couchdb.service';
import { forkJoin, throwError, of } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
import { PlanetMessageService } from '../shared/planet-message.service';
import { UserService } from '../shared/user.service';
import { SyncService } from '../shared/sync.service';
import { findDocuments } from '../shared/mangoQueries';
import { ManagerService } from './manager.service';
import { StateService } from '../shared/state.service';
import { ReportsService } from './reports/reports.service';
import { DialogsLoadingService } from '../shared/dialogs/dialogs-loading.service';

@Directive({
  selector: '[planetSync]'
})
export class SyncDirective {
  @Output() syncComplete = new EventEmitter<void>();

  planetConfiguration = this.stateService.configuration;

  constructor(
    private couchService: CouchService,
    private userService: UserService,
    private syncService: SyncService,
    private planetMessageService: PlanetMessageService,
    private managerService: ManagerService,
    private stateService: StateService,
    private reportsService: ReportsService,
    private dialogsLoadingService: DialogsLoadingService
  ) {}

  @HostListener('click')
  runSyncClick() {
    this.dialogsLoadingService.start();
    this.updateReplicatorUsers().subscribe(() => {
      this.syncPlanet();
    });
  }

  syncPlanet() {
    const defaultList = this.replicatorList((type) => (val) => this.syncService.replicatorId(val, type));
    const deleteArray = (replicators) => replicators.filter(rep => {
      return rep._replication_state === 'completed' || defaultList.indexOf(rep._id) > -1;
    }).map(rep => {
      return { ...rep, _deleted: true };
    });
    this.couchService.findAll('_replicator').pipe(
      switchMap((replicators) => this.syncService.deleteReplicators(deleteArray(replicators))),
      switchMap(() => forkJoin(this.sendStatsToParent(), this.getParentUsers())),
      map(([ res, users ]) => this.updateParentUsers(users)),
      switchMap(() => this.getAchievementsAndTeamAndNewsResources()),
      switchMap(([ achievements, teamResources, news ]: any[]) =>
        forkJoin(this.achievementResourceReplicator(achievements), this.teamAndNewsResourcesReplicator(teamResources, news))
      ),
      switchMap((replicators: any) => {
        this.dialogsLoadingService.stop();
        return this.syncService.confirmPasswordAndRunReplicators(this.replicatorList().concat(replicators.flat(2)));
      }),
      switchMap(res => this.managerService.addAdminLog('sync'))
    ).subscribe(data => {
      this.planetMessageService.showMessage($localize`Syncing started`);
      this.syncComplete.emit();
    }, error => this.planetMessageService.showMessage(error.error ? error.error.reason : error));
  }

  replicatorList(mapFunc = (type) => (val) => ({ continuous: this.planetConfiguration.alwaysOnline, ...val, type })) {
    const bothList = [
      { db: 'submissions', selector: { source: this.planetConfiguration.code } },
      { db: 'teams', selector: { '$or': [ { teamType: 'sync' }, { docType: 'link' } ], teamPlanetCode: this.planetConfiguration.code } },
      { db: 'news', selector: { '$or': [
        { messageType: 'sync', messagePlanetCode: this.planetConfiguration.code },
        { viewIn: { '$elemMatch': { '_id': `${this.planetConfiguration.code}@${this.planetConfiguration.parentCode}` } } }
      ] } },
      { db: 'team_activities', selector: { teamType: 'sync', teamPlanetCode: this.planetConfiguration.code } },
      { db: 'tasks', selector: { 'sync.type': 'sync', 'sync.planetCode': this.planetConfiguration.code } },
      { db: 'meetups', selector: { 'sync.type': 'sync', 'sync.planetCode': this.planetConfiguration.code } }
    ];
    const pushList = [ ...this.pushList(), ...bothList ];
    const pullList = [ ...this.pullList(), ...bothList ];
    const internalList = [
      { dbSource: '_users', db: 'tablet_users', selector: { 'isUserAdmin': false, 'requestId': { '$exists': false } }, continuous: true },
      { dbSource: 'meetups', db: 'community_meetups', selector: { 'link': { 'teams': { '$eq': `${this.planetConfiguration.code}@${this.planetConfiguration.parentCode}` } } }, continuous: true }
    ];
    return pushList.map(mapFunc('push')).concat(pullList.map(mapFunc('pull'))).concat(internalList.map(mapFunc('internal')));
  }

  pushList() {
    return [
      { db: 'courses_progress' },
      { db: 'feedback' },
      { db: 'login_activities' },
      { db: 'ratings' },
      { db: 'resource_activities' },
      { dbSource: 'replicator_users', dbTarget: 'child_users' },
      { db: 'admin_activities' },
      { db: 'achievements', selector: { sendToNation: true, createdOn: this.planetConfiguration.code } },
      { db: 'apk_logs' },
      { db: 'myplanet_activities' },
      { db: 'notifications', selector: { userPlanetCode: this.planetConfiguration.parentCode } },
      { db: 'attachments', selector: { planetCode: this.planetConfiguration.code } },
      { db: 'course_activities' },
      { db: 'search_activities' },
      { db: 'health', selector: { planetCode: this.planetConfiguration.code } },
    ];
  }

  pullList() {
    return [
      { db: 'feedback', selector: { source: this.planetConfiguration.code } },
      { db: 'notifications', selector: { userPlanetCode: this.planetConfiguration.code } },
      { db: 'attachments', selector: { planetCode: this.planetConfiguration.parentCode } }
    ];
  }

  updateReplicatorUsers() {
    const userProperties = this.userService.userProperties.filter(prop => prop !== 'requestId');
    return forkJoin([
      this.couchService.findAll('_users', findDocuments(
        { 'isUserAdmin': { '$exists': true }, 'requestId': { '$exists': false } },
        userProperties
      )),
      this.couchService.findAll('replicator_users', findDocuments({}, [ ...userProperties, 'couchId', 'planetCode' ]))
    ]).pipe(
      switchMap(([ users, repUsers ]) => {
        const newRepUsers = this.createReplicatorUserDoc(users, repUsers);
        const deletedRepUsers = repUsers
          .filter((rUser: any) => users.findIndex((user: any) => rUser.couchId === user._id) < 0)
          .map((rUser: any) => ({ ...rUser, '_deleted': true }));
        return this.couchService.post('replicator_users/_bulk_docs', { docs: newRepUsers.concat(deletedRepUsers) });
      })
    );
  }

  createReplicatorUserDoc(users: any[], repUsers: any[]) {
    const planetCode = this.planetConfiguration.code;
    return users.map((user: any) => {
      const repUser = repUsers.find((rUser: any) => rUser.couchId === user._id) || {},
        { _id, _rev, ...userProps } = user;
      return {
        ...repUser,
        ...userProps,
        _id: user.name + '@' + planetCode,
        couchId: user._id,
        planetCode: planetCode
      };
    });
  }

  sendStatsToParent() {
    const { code, parentDomain: domain } = this.planetConfiguration;
    return forkJoin([
      this.reportsService.getDatabaseCount('resources'),
      this.reportsService.getDatabaseCount('courses'),
      this.getChildStats(code, domain)
    ]).pipe(switchMap(([ totalResources, totalCourses, stats ]) => {
      const { error, reason, docs, rows, ...statsDoc } = stats;
      return this.couchService.post('child_statistics', { _id: code, ...statsDoc, totalCourses, totalResources }, { domain });
    }));
  }

  getChildStats(code, domain) {
    return this.couchService.get('child_statistics/' + code, { domain }).pipe(catchError((err) => {
      if (err.error.reason === 'missing') {
        return of({});
      }
      return throwError(err);
    }));
  }

  achievementResourceReplicator(achievements: any[]) {
    return this.syncService.replicatorsArrayWithTags(
      achievements.filter(a => a.sendToNation === true).map(a => ({ db: 'achievements', item: a })), 'push', 'local'
    ).pipe(map(replicators => replicators.filter(rep => rep.db !== 'achievements')));
  }

  getParentUsers() {
    return this.couchService.findAll(
      '_users',
      findDocuments({ planetCode: this.planetConfiguration.parentCode }),
      { domain: this.planetConfiguration.parentDomain }
    );
  }

  updateParentUsers(newUsers: any[]) {
    this.couchService.findAll('parent_users').pipe(switchMap((oldUsers: any[]) => {
      const deleteArray = oldUsers
        .filter(oldUser => !newUsers.some(newUser => newUser._id === oldUser._id))
        .map(oldUser => ({ ...oldUser, _deleted: true }));
      const updateArray = newUsers.map(newUser => {
        const oldUser = oldUsers.find(old => newUser._id === old._id);
        return { ...newUser, _rev: oldUser ? oldUser._rev : undefined };
      });
      const docs = [ ...deleteArray, ...updateArray ].map(({ _attachments, ...doc }) => doc);
      return this.couchService.bulkDocs('parent_users', docs);
    })).subscribe();
  }

  getAchievementsAndTeamAndNewsResources() {
    return forkJoin([
      this.couchService.findAll('achievements', findDocuments({ sendToNation: true, createdOn: this.planetConfiguration.code })),
      this.couchService.findAll(
        'teams', findDocuments({ docType: 'resourceLink', teamType: 'sync', teamPlanetCode: this.planetConfiguration.code })
      ),
      this.couchService.findAll('news', findDocuments({ images: { '$exists': true }, messageType: 'sync' }))
    ]);
  }

  teamAndNewsResourcesReplicator(teamResources: any[], news: any[]) {
    const replicator = linkDoc => ({ db: 'resources', item: { _id: linkDoc.resourceId } });
    return forkJoin([
      this.syncService.replicatorsArrayWithTags(
        [
          ...teamResources.map(replicator),
          ...news.map(post => post.images.map(replicator)).flat()
        ],
        'push',
        'local'
      ),
      this.syncService.replicatorsArrayWithTags(
        news.filter(post => post.createdOn !== this.planetConfiguration.code).map(post => post.images.map(replicator)).flat(),
        'pull',
        'parent'
      )
    ]);
  }

}