Home Reference Source

src/contacts-new/ContactRepository.ts

import Logs from '../Logs';
import {zip} from 'lodash-es';
import {combineLatest, concat, defer, Observable, of} from 'rxjs';
import {debounceTime, map, scan, shareReplay, switchMap, tap} from 'rxjs/operators';
import {User} from '../users-new/User';
import {UserRepository} from '../users-new/UserRepository';
import {ContactService} from '../wac-proxy/wac-stack/contact/ContactService';
import {AddressBookType, AddressTypeDto, ContactEditionDto, ContactDto, ContactCreationDto} from '../wac-proxy/wac-stack/contact/types';
import {AddressType} from './AddressType';
import {Contact} from './Contact';
import {ContactType} from './ContactType';

const log = Logs.instance.getLogger('SippoJS/ContactRepository');

const toContactType = (type: AddressBookType): ContactType => {
	switch (type) {
		case AddressBookType.CONTACTS: return ContactType.CONTACTS;
		case AddressBookType.DOMAIN: return ContactType.DOMAIN;
		case AddressBookType.GROUPS: return ContactType.GROUPS;
		case AddressBookType.PHONEBOOKS: return ContactType.PHONEBOOKS;
		case AddressBookType.STATIC: return ContactType.STATIC;
	}
};

const toAddressType = (type: AddressTypeDto): AddressType => {
	switch (type) {
		case AddressTypeDto.HOME: return AddressType.HOME;
		case AddressTypeDto.OTHER: return AddressType.OTHER;
		case AddressTypeDto.WORK: return AddressType.WORK;
	}
};

const toContact = (data: ContactDto & {type: AddressBookType}, user?: User): Contact => ({
	id: data.id,
	name: data.name,
	type: toContactType(data.type),
	phones: data.phones.map(phone => ({...phone, type: toAddressType(phone.type)})),
	emails: data.emails.map(email => ({...email, type: toAddressType(email.type)})),
	favorite: data.favorite || false,
	editable: toContactType(data.type) === ContactType.CONTACTS,
	user,
});

const toContactEditionAddressName = (type: AddressType): string => {
	switch (type) {
		case AddressType.HOME: return 'home';
		case AddressType.OTHER: return 'other';
		case AddressType.WORK: return 'work';
	}
};

const toContactEditionDto = (contact: Pick<Contact, 'id'|'name'|'favorite'|'phones'|'emails'>): ContactEditionDto => ({
	id: contact.id,
	name: contact.name,
	favorite: contact.favorite,
	phones: contact.phones.map(({type, value}) => ({name: toContactEditionAddressName(type), address: value})),
	emails: contact.emails.map(({type, value}) => ({name: toContactEditionAddressName(type), email: value})),
	source: 'wac',
});

const toContactCreationDto = (contact: Pick<Contact, 'name'|'favorite'|'phones'|'emails'>): ContactCreationDto => ({
	id: '',
	name: contact.name,
	favorite: contact.favorite,
	phones: contact.phones.map(({type, value}) => ({name: toContactEditionAddressName(type), address: value})),
	emails: contact.emails.map(({type, value}) => ({name: toContactEditionAddressName(type), email: value})),
	source: 'wac',
});

type Action = {
	type: 'INIT' | 'CREATE' | 'UPDATE' | 'DELETE';
	contacts: Readonly<Array<ContactDto & {type: AddressBookType}>>;
};
type State = Readonly<Array<ContactDto & {type: AddressBookType}>>;

/**
 * Allows retrieving, updating and creating contacts. A ContactRepository instance
 * is obtained calling the {@link Session#getContactRepository} method and must not
 * be directly instantiated.
 */
export class ContactRepository {
	readonly contacts$: Observable<readonly Contact[]>;

	private contactService: ContactService;
	private userRepository: UserRepository;

	/** @ignore */
	constructor(contactService: ContactService, userRepository: UserRepository) {
		/** @private */
		this.contactService = contactService;
		/** @private */
		this.userRepository = userRepository;
		/**
		 * Allows access to the list with every contact
		 * @type {Observable<Contact[]>}
		 */
		this.contacts$ = this.getActions().pipe(
			scan((state: State, action: Action) => {
				log.debug('action', action);
				switch (action.type) {
					case 'INIT':
						return action.contacts;
					case 'CREATE':
						return state.concat(action.contacts);
					case 'DELETE':
						return state.filter(contact => !action.contacts.some(c => c.id === contact.id));
					case 'UPDATE':
						return state.map(contact => action.contacts.find(c => c.id === contact.id) || contact);
				}
			}, []),
			switchMap((contacts) => {
				if (contacts.length === 0) {
					return of([[], []]);
				}
				const users = contacts.map(contact =>
					contact.associatedUserId ? this.userRepository.getUser$(contact.associatedUserId) : of(undefined),
				);
				return combineLatest([
					of(contacts),
					combineLatest(users).pipe(debounceTime(10)),
				]);
			}),
			map(([contacts, users]) => zip(contacts, users)
				.map(([contact, user]) => contact ? toContact(contact, user) : undefined)
				.filter((contact): contact is Contact => contact !== undefined)),
			tap(x => log.debug('contacts', x)),
			shareReplay(1),
		);
	}

	/** @private */
	private getActions(): Observable<Action> {
		const initialAction$: Observable<Action> = defer(() => this.contactService.getContacts$()).pipe(
			map(addressBooks => ({
				type: 'INIT',
				contacts: addressBooks.flatMap(
					addressBook => addressBook.contacts.map(contact => ({...contact, type: addressBook.type})),
				),
			})),
		);
		const updatedActions$: Observable<Action> = defer(() => this.contactService.contactEvent$).pipe(
			map((event) => {
				const contacts = [
					{...event.body.contact, type: event.body.addressBookType},
				];
				switch (event.method) {
					case 'POST': return {type: 'CREATE', contacts};
					case 'PUT': return {type: 'UPDATE', contacts};
					case 'DELETE': return {type: 'DELETE', contacts};
				}
			}),
		);
		return concat(initialAction$, updatedActions$);
	}

	/**
	 * Saves a contact
	 * @param {Contact} contact
	 * @return {Promise<void>}
	 */
	saveContact(contact: Pick<Contact, 'id'|'name'|'phones'|'emails'|'favorite'>): Promise<void> {
		const data = toContactEditionDto(contact);
		return this.contactService.updateContact(data);
	}

	/**
	 * Creates a new contact
	 * @param {{name: string, phones: Phone[], emails: Email[], favorite: boolean = false}} contact
	 * @return {Promise<void>}
	 */
	createContact(contact: Pick<Contact, 'name'|'phones'|'emails'|'favorite'>): Promise<void> {
		const data = toContactCreationDto(contact);
		return this.contactService.createContact(data);
	}

	/**
	 * Deletes a contact
	 * @param {{id: string}} param
	 * @return {Promise<void>}
	 */
	deleteContact({id}: Pick<Contact, 'id'>): Promise<void> {
		return this.contactService.deleteContact(id);
	}
}