Home Reference Source

packages/skygear-core/lib/relation.js

/**
 * Copyright 2015 Oursky Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const _ = require('lodash');

import {UserRecord} from './container';

/**
 * Outward relation direction
 *
 * @type {String}
 */
export const Outward = 'outward';

/**
 * Inward relation direction
 *
 * @type {String}
 */
export const Inward = 'inward';

/**
 * Mutual relation direction
 *
 * @type {String}
 */
export const Mutual = 'mutual';

const format = /^[a-zA-Z]+$/;

/**
 * Relation
 *
 * It describes a relationship of the current user with other users.
 */
export class Relation {

  /**
   * Constructs a new Relation object.
   *
   * @param  {String} identifier - identifier of the relation
   * @param  {String} direction - direction of the relation
   * @param  {Record[]} targets - target users of the relation
   */
  constructor(identifier, direction, targets = []) {
    if (!Relation.validName(identifier)) {
      throw new Error(
        'Relation identifier can only be [a-zA-Z]+');
    }
    this.identifier = identifier;
    if (Relation.validDirection(direction)) {
      this.direction = direction;
    } else {
      throw new Error(
        'Relation direction not supported.');
    }
    this.targets = targets;
    this.fails = [];
  }

  /**
   * Target user ids
   *
   * @type {String[]}
   */
  get targetsID() {
    return _.map(this.targets, function (user) {
      return user._id;
    });
  }

  /**
   * @private
   */
  static validDirection(direction) {
    return direction === Mutual
      || direction === Outward
      || direction === Inward;
  }

  /**
   * @private
   */
  static validName(identifier) {
    return format.test(identifier);
  }

  /**
   * @private
   */
  static extend(identifier, direction) {
    if (!Relation.validName(identifier)) {
      throw new Error(
        'Relation identifier can only be [a-zA-Z]+');
    }
    let RelationProto = {
      identifier: identifier,
      direction: direction
    };
    function RelationCls(targets = []) {
      Relation.call(this, identifier, direction);
      this.targets = targets;
    }
    RelationCls.prototype = _.create(Relation.prototype, RelationProto);
    return RelationCls;
  }
}

/**
 * @private
 */
export class RelationQuery {

  constructor(relationCls) {
    this.identifier = relationCls.prototype.identifier;
    this.direction = relationCls.prototype.direction;
    this.limit = 50;
    this.page = 0;
  }

  toJSON() {
    return {
      name: this.identifier,
      direction: this.direction,
      limit: this.limit,
      page: this.page
    };
  }

}

/**
 * Result of add relation API
 */
export class RelationResult {

  constructor(results) {
    /**
     * Succesfully added target users
     *
     * @type {Record[]}
     */
    this.success = [];

    /**
     * Errors
     *
     * @type {Object[]}
     */
    this.fails = [];
    this.partialError = false;
    let len = results.length;
    for (let i = 0; i < len; i++) {
      if (results[i].type === 'error') {
        this.fails.push(results[i]);
        this.partialError = true;
      } else {
        this.success.push(new UserRecord(results[i].data));
      }
    }
  }

}

/**
 * Result of remove relation API
 */
export class RelationRemoveResult {

  constructor(results) {
    /**
     * Succesfully removed target users
     *
     * @type {String[]}
     */
    this.success = [];

    /**
     * Errors
     *
     * @type {Object[]}
     */
    this.fails = [];
    this.partialError = false;
    let len = results.length;
    for (let i = 0; i < len; i++) {
      if (results[i].type === 'error') {
        this.fails.push(results[i]);
        this.partialError = true;
      } else {
        this.success.push(results[i].id);
      }
    }
  }

}

/**
 * Result of query relation API
 */
export class RelationQueryResult extends Array {

  /**
   * @private
   */
  static createFromBody(body) {
    let users = _.map(body.result, function (attrs) {
      return new UserRecord(attrs.data);
    });
    let result = new RelationQueryResult();
    users.forEach((val) => result.push(val));
    let info = body.info;
    result._overallCount = info ? info.count : undefined;
    return result;
  }

  /**
   * The count would return the number of all matching records, and ignore the
   * offset and limit of the query.
   *
   * @type {Number} the number of all matching records
   */
  get overallCount() {
    return this._overallCount;
  }

}

export class RelationContainer {

  constructor(container) {
    this.container = container;
  }

  /**
   * Queries users with a relation query object.
   *
   * @param  {RelationQuery} queryObj
   * @return {Promise<RelationQueryResult>} promise with user records
   */
  async query(queryObj) {
    const body = await this.container.makeRequest(
      'relation:query',
      queryObj.toJSON()
    );
    return RelationQueryResult.createFromBody(body);
  }

  /**
   * Queries friends of current user. Convenient method of
   * {@link RelationContainer#query}.
   *
   * @return {Promise<RelationQueryResult>} promise with user records
   */
  async queryFriend() {
    let query = new RelationQuery(this.Friend);
    return this.query(query);
  }

  /**
   * Queries followers of current user. Convenient method of
   * {@link RelationContainer#query}.
   *
   * @return {Promise<RelationQueryResult>} promise with user records
   */
  async queryFollower() {
    let query = new RelationQuery(this.Follower);
    return this.query(query);
  }

  /**
   * Queries users that the current user is following. Convenient method of
   * {@link RelationContainer#query}.
   *
   * @return {Promise<RelationQueryResult>} promise with user records
   */
  async queryFollowing() {
    let query = new RelationQuery(this.Following);
    return this.query(query);
  }

  /**
   * Adds relation to the current user.
   *
   * @param {Relation} relation
   * @return {Promise<RelationResult>} promise with user records
   */
  async add(relation) {
    const body = await this.container.makeRequest('relation:add', {
      name: relation.identifier,
      direction: relation.direction,
      targets: relation.targetsID
    });
    return new RelationResult(body.result);
  }

  /**
   * Removes relation from the current user.
   *
   * @param {Relation} relation
   * @return {Promise<RelationRemoveResult>} promise with user id
   */
  async remove(relation) {
    const body = await this.container.makeRequest('relation:remove', {
      name: relation.identifier,
      direction: relation.direction,
      targets: relation.targetsID
    });
    return new RelationRemoveResult(body.result);
  }

  /**
   * Relation query class.
   *
   * @type {RelationQuery}
   */
  get Query() {
    return RelationQuery;
  }

  /**
   * @type {Relation}
   */
  get Friend() {
    return Relation.extend('friend', Mutual);
  }

  /**
   * @type {Relation}
   */
  get Follower() {
    return Relation.extend('follow', Inward);
  }

  /**
   * @type {Relation}
   */
  get Following() {
    return Relation.extend('follow', Outward);
  }

}