Source: pages.js

/* eslint-disable max-classes-per-file */

const Job = require('./job.js');
const Service = require('./service.js');
const UserFile = require('./userfile.js');
const UserProcess = require('./userprocess.js');
const Utils = require('@openeo/js-commons/src/utils');
const StacMigrate = require('@radiantearth/stac-migrate');

const FED_MISSING = 'federation:missing';

/**
 * A class to handle pagination of resources.
 * 
 * @abstract
 */
class Pages {
  /**
   * Creates an instance of Pages.
   *
   * @param {Connection} connection
   * @param {string} endpoint
   * @param {string} key
   * @param {Constructor} cls
   * @param {object} [params={}]
   * @param {string} primaryKey
   */
  constructor(connection, endpoint, key, cls, params = {}, primaryKey = "id") {
    this.connection = connection;
		this.nextUrl = endpoint;
    this.key = key;
    this.primaryKey = primaryKey;
    this.cls = cls;
    if (!(params.limit > 0)) {
      delete params.limit;
    }
    this.params = params;
  }

  /**
   * Returns true if there are more pages to fetch.
   *
   * @returns {boolean}
   */
  hasNextPage() {
    return this.nextUrl !== null;
  }

  /**
   * Returns the next page of resources.
   * 
   * @async
   * @param {Array.<object>} oldObjects - Existing objects to update, if any.
   * @param {boolean} [toArray=true] - Whether to return the objects as a simplified array or as an object with all information.
   * @returns {Array.<object>}
   * @throws {Error}
   */
  async nextPage(oldObjects = [], toArray = true) {
    // Request data from server
    const response = await this.connection._get(this.nextUrl, this.params);

    let data = response.data;
    // Check response
    if (!Utils.isObject(data)) {
      throw new Error(`Response is invalid, is not an object`);
    }
    if (!Array.isArray(data[this.key])) {
      throw new Error(`Response is invalid, '${this.key}' property is not an array`);
    }

    // Update existing objects if needed
    let newObjects = data[this.key].map(updated => {
      let resource = oldObjects.find(old => old[this.primaryKey] === updated[this.primaryKey]);
      if (resource) {
        resource.setAll(updated);
      }
      else {
        resource = this._createObject(updated);
      }
      return resource;
    });

    // Store objects in cache if needed
    newObjects = this._cache(newObjects);

    // Add self link if missing
    data.links = this._ensureArray(data.links);
    const selfLink = this.connection._getLinkHref(data.links, 'self');
    if (!selfLink) {
      data.links.push({rel: 'self', href: this.nextUrl});
    }

    // Check whether a next page is available
    this.nextUrl = this._getNextLink(response);
    // Don't append initial params to the next URL
    this.params = null;

    // Either return as ResponseArray or full API response body
    if (toArray) {
      newObjects.links = data.links;
      newObjects[FED_MISSING] = this._ensureArray(data[FED_MISSING]);
      return newObjects;
    }
    else {
      data[this.key] = newObjects;
      return data;
    }
  }

  /**
   * Ensures a variable is an array.
   * 
   * @protected
   * @param {*} x 
   * @returns {Array}
   */
  _ensureArray(x) {
    return Array.isArray(x) ? x : [];
  }

  /**
   * Creates a facade for the object, if needed.
   *
   * @protected
   * @param {object} obj 
   * @returns {object}
   */
  _createObject(obj) {
    if (this.cls) {
      const cls = this.cls;
      const newObj = new cls(this.connection, obj[this.primaryKey]);
      newObj.setAll(obj);
      return newObj;
    }
    else {
      return obj;
    }
  }

  /**
   * Caches the plain objects if needed.
   * 
   * @param {Array.<object>} objects
   * @returns {Array.<object>}
   */
  _cache(objects) {
    return objects;
  }
  
  /**
   * Get the URL of the next page from a response.
   * 
   * @protected
   * @param {AxiosResponse} response 
   * @returns {string | null}
   */
  _getNextLink(response) {
    const links = this.connection.makeLinksAbsolute(response.data.links, response);
    return this.connection._getLinkHref(links, 'next');
  }
  
  /**
   * Makes this class asynchronously iterable.
   *
   * @returns {AsyncIterator}
   */
  [Symbol.asyncIterator]() {
    return {
      self: this,
      /**
       * Get the next page of resources.
       *
       * @async
       * @returns {{done: boolean, value: Array.<object>}}
       */
      async next() {
        const done = !this.self.hasNextPage();
        let value;
        if (!done) {
          value = await this.self.nextPage();
        }
        return { done, value };
      }
    }
  }

}

/**
 * Paginate through jobs.
 */
class JobPages extends Pages {
  /**
   * Paginate through jobs.
   *
   * @param {Connection} connection
   * @param {?number} limit
   */
  constructor(connection, limit = null) {
    super(connection, "/jobs", "jobs", Job, {limit});
  }
}

/**
 * Paginate through services.
 */
class ServicePages extends Pages {
  /**
   * Paginate through services.
   *
   * @param {Connection} connection
   * @param {?number} limit
   */
  constructor(connection, limit = null) {
    super(connection, "/services", "services", Service, {limit});
  }
}

/**
 * Paginate through user files.
 */
class UserFilePages extends Pages {
  /**
   * Paginate through user files.
   *
   * @param {Connection} connection
   * @param {?number} limit
   */
  constructor(connection, limit = null) {
    super(connection, "/files", "files", UserFile, {limit}, "path");
  }
}

/**
 * Paginate through processes.
 */
class ProcessPages extends Pages {
  /**
   * Paginate through processes.
   *
   * @param {Connection} connection
   * @param {?number} limit
   * @param {?string} namespace
   */
  constructor(connection, limit = null, namespace = null) {
    if (!namespace) {
			namespace = 'backend';
		}
    let endpoint;
    let cls = null
    if (namespace === 'user') {
      endpoint = '/process_graphs';
      cls = UserProcess;
    }
    else {
      endpoint = '/processes';
      if (namespace !== 'backend') {
        const normalized = connection.normalizeNamespace(namespace);
        endpoint += `/${normalized}`;
      }
    }
    super(connection, endpoint, "processes", cls, {limit});
    this.namespace = namespace;
  }

  /**
   * Caches the objects to the ProcessRegistry.
   * 
   * @param {Array.<object>} objects
   * @returns {Array.<object>}
   */
  _cache(objects) {
    const plainObjects = objects.map(p => (typeof p.toJSON === 'function' ? p.toJSON() : p));
    this.connection.processes.addAll(plainObjects, this.namespace);
    if (!this.cls) {
      for (let i in objects) {
        objects[i] = this.connection.processes.get(objects[i].id, this.namespace);
      }
    }
    return objects;
  }
}

/**
 * Paginate through collections.
 */
class CollectionPages extends Pages {
  /**
   * Paginate through collections.
   *
   * @param {Connection} connection
   * @param {?number} limit
   */
  constructor(connection, limit = null) {
    super(connection, "/collections", "collections", null, {limit});
  }

  /**
   * Migrates the STAC collection to the latest version.
   * 
   * @param {object} obj 
   * @returns {Collection}
   */
  _createObject(obj) {
    if (obj.stac_version) {
      return StacMigrate.collection(obj);
    }
    return obj;
  }
}

/**
 * Paginate through collection items.
 */
class ItemPages extends Pages {
  /**
   * Paginate through collection items.
   *
   * @param {Connection} connection
   * @param {string} collectionId
   * @param {object} params
   */
  constructor(connection, collectionId, params) {
    super(connection, `/collections/${collectionId}/items`, "features", null, params);
  }

  /**
   * Migrates the STAC item to the latest version.
   * 
   * @param {object} obj 
   * @returns {Item}
   */
  _createObject(obj) {
    if (obj.stac_version) {
      return StacMigrate.item(obj);
    }
    return obj;
  }
}

module.exports = {
  Pages,
  CollectionPages,
  ItemPages,
  JobPages,
  ProcessPages,
  ServicePages,
  UserFilePages
}