Source: query.js

import queryString from 'qs';
import Predications from './predications';

/**
 * A fluent interace for creating queries against resourceful endpoints
 *
 * @param {string?} query - (private) Initial query string to append to.
 * @param {string?} sort - (private) What to sort the query on
 * @param {string} [sortModifier=''] - (private) Sort modifier. Either empty string or '-'
 *
 * @requires {@link Predications}
 *
 * @example
 * import { where, field } from '@pikselpalette/sequoia-js-client-sdk/lib/query';
 *
 * endpoint.browse(where(field('startedAt').greaterThanOrEqualTo(2015))
 *                 .and(field('tags').equalTo('showcase'))
 *                 .fields('title', 'mediumSynopsis','duration', 'ref')
 *                 .include('assets').page(1).perPage(24).orderByUpdatedAt().desc().count())
 *         .then(json => { ... });
 *
 */
class Query {
  constructor(query) {
    this.query = query || '';
    this.sort = null;
    this.sortModifier = '';
  }

  /**
   * Concatenate a new query with the previous one
   *
   * @param {string} query - A query string to append (without leading ampersand).
   * Usually returned from a call to `field`
   *
   * @example
   * where(field('startedAt').greaterThanOrEqualTo(2015))
   * .and(field('tags').equalTo('showcase'))
   *
   * @since 0.0.2
   *
   * @returns {Query}
   */
  and(query) {
    this.query += `&${query}`;
    return this;
  }

  /**
   * Will return `totalCount` on the payload
   *
   * @returns {Query}
   */
  count() {
    this.query += '&count=true';
    return this;
  }

  /**
   * Appends `continue=true` to the query to initiate continuation paging
   *
   * @returns {Query}
   */
  continue() {
    this.query += '&continue=true';
    return this;
  }

  /**
   * Appends `include=value1,value2` to the query
   *
   * @param {...string} includes - includes(s) (linked resources) to return in the json response
   *
   * @example
   * where().include('assets', 'categories')
   * // appends `include=assets,categories` to the query
   *
   * @returns {Query}
   */
  include(...includes) {
    this.query += `&include=${includes.join(',')}`;
    return this;
  }

  /**
   * Appends `lang=value` to the query
   *
   * @param {string} value - ISO 639-1 code for the language you want results returned in
   *
   * @example
   * where().lang('de')
   * // appends `lang=de` to the query
   *
   * @returns {Query}
   */
  lang(value) {
    this.query += `&lang=${value}`;
    return this;
  }

  /**
   * Appends `fields=value1,value2` to the query
   *
   * @param {...string} fieldName - field(s) to return in the json response
   *
   * @example
   * where().fields('title', 'mediumSynopsis','duration', 'ref')
   * // appends `fields=title,mediumSynopsis,duration,ref` to the query
   *
   * @returns {Query}
   */
  fields(...fieldName) {
    this.query += `&fields=${fieldName.join(',')}`;
    return this;
  }

  /**
   * Appends `page=value` to the query
   *
   * @param {(string|number)} value - page of results to request
   *
   * @example
   * where().page(9)
   * // appends `page=9` to the query
   *
   * @returns {Query}
   */
  page(value) {
    this.query += `&page=${value}`;
    return this;
  }

  /**
   * Appends `perPage=value` to the query
   *
   * @param {(string|number)} value - how many items to return per page of results
   *
   * @example
   * where().perPage(24)
   * // appends `perPage=24` to the query
   *
   * @returns {Query}
   */
  perPage(value) {
    this.query += `&perPage=${value}`;
    return this;
  }

  /**
   * Set order to ascending
   *
   * @returns {Query}
   */
  asc() {
    this.sortModifier = '';
    return this;
  }

  /**
   * Set order to descending
   *
   * @returns {Query}
   */
  desc() {
    this.sortModifier = '-';
    return this;
  }

  /**
   * Appends `sort=[-]fieldName` to the query, where `[-]` is toggled
   * depending on the call to {@link Query#asc} or {@link Query#desc}
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderBy(fieldName) {
    this.sort = fieldName;
    return this;
  }

  /**
   * Look through the relationships of a criteria, and include fields from through
   * relationships - this makes it so that the end user shouldn't know or care that
   * through relationships exist.
   *
   * @param {relationships} relationships - The relationship definitions that may
   * require the addition of fields to support the graphical display of a through relationship
   *
   * @returns {Query}

   */
  addRelatedThroughFields(relationships = {}, allFields = []) {
    const parsedQueryString = queryString.parse(this.query, {
      ignoreQueryPrefix: true,
      allowDots: true
    });
    const includes = Object.keys(parsedQueryString).includes('include')
      ? parsedQueryString.include.split(',')
      : [];
    let fields = Object.keys(parsedQueryString).includes('fields')
      ? parsedQueryString.fields.split(',')
      : [];

    const filteredIncludes = includes
      .map((include) => {
        const { through } = relationships[include] || '';
        const throughRelationship = relationships[through] || {};

        return throughRelationship.fieldNamePath || false;
      })
      .filter(fieldNamePath => fieldNamePath);

    fields = filteredIncludes.length
      ? (fields.length ? fields : allFields).concat(filteredIncludes)
      : fields;

    parsedQueryString.fields = fields.length ? fields.join(',') : undefined;

    this.query = queryString.stringify(parsedQueryString, { encode: false, allowDots: true, indices: false });
    return this;
  }

  /**
   * Convenience method for ordering by 'owner'
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderByOwner() {
    return this.orderBy('owner');
  }

  /**
   * Convenience method for ordering by 'name'
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderByName() {
    return this.orderBy('name');
  }

  /**
   * Convenience method for ordering by 'createdAt'
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderByCreatedAt() {
    return this.orderBy('createdAt');
  }

  /**
   * Convenience method for ordering by 'createdBy'
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderByCreatedBy() {
    return this.orderBy('createdBy');
  }

  /**
   * Convenience method for ordering by 'updatedAt'
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderByUpdatedAt() {
    return this.orderBy('updatedAt');
  }

  /**
   * Convenience method for ordering by 'updatedBy'
   *
   * @see {@link Query#orderBy}
   *
   * @returns {Query}
   */
  orderByUpdatedBy() {
    return this.orderBy('updatedBy');
  }

  /**
   * Turn the current Query into a string representation of a URI query
   *
   * @returns {string}
   */
  toQueryString() {
    let { query } = this;

    if (this.sort) {
      query += `&sort=${this.sortModifier}${this.sort}`;
    }

    return query;
  }
}

export default Query;

export function where(criteria) {
  return new Query(criteria);
}

export function field(value) {
  return new Predications(value);
}

export function param(value) {
  return new Predications(value, true);
}

export function textSearch(value) {
  return `q=${value}`;
}