holidayextras/jsonapi-store-elasticsearch

View on GitHub
lib/elasticsearchHandler.js

Summary

Maintainability
C
1 day
Test Coverage
"use strict";
var ElasticsearchStore = module.exports = function ElasticsearchStore() { };

var async = require("async");
var debug = require("debug")("jsonApi:store:elasticsearch");
var elasticsearch = require("elasticsearch");
var _ = {
  assign: require("lodash.assign")
};


ElasticsearchStore.prototype.ready = false;

ElasticsearchStore.prototype._buildQuery = function(request) {
  var self = this;

  var queryString = "type:" + request.params.type;
  if (!request.params.filter) return queryString;

  queryString = "(" + queryString + ")";
  var filterString = Object.keys(request.params.filter).map(function(attribute) {
    var attributeConfig = self.resourceConfig.attributes[attribute];
    // If the filter attribute doens't exist, skip it
    if (!attributeConfig) return null;

    var values = request.params.filter[attribute];
    if (!values) return null;

    // Relationships need to be queried via .id
    if (attributeConfig._settings) {
      attribute += ".id";
      // Filters on nested resources should be skipped
      if (values instanceof Object) return null;
    }

    // Coerce values to an array to simplify the logic
    values = [].concat(values);
    values = values.map(function(value) {
      if (value[0] === ":" || (value[0] === "~")) return value.substring(1);
      return value;
    }).join(" OR ");

    return attribute + ":(" + values + ")";
  }).filter(function(value) {
    return value !== null;
  });
  if (filterString.length > 0) {
    queryString += " AND (" + filterString.join(") AND (") + ")";
  }

  return queryString;
};

ElasticsearchStore.prototype._applySort = function(request) {
  if (!request.params.sort) return { };

  var attribute = request.params.sort;
  var order = "asc";
  attribute = String(attribute);
  if (attribute[0] === "-") {
    order = "desc";
    attribute = attribute.substring(1, attribute.length);
  }
  var sortParam = {
    sort: attribute + ".raw:" + order
  };

  return sortParam;
};


ElasticsearchStore.prototype._applyPagination = function(request) {
  if (!request.params.page) return { };

  var page = {
    size: request.params.page.limit,
    from: request.params.page.offset
  };

  return page;
};

ElasticsearchStore.prototype._generateMappingFor = function(resourceConfig) {
  var mapping = { properties: {
    id: {
      type: "string"
    },
    type: {
      type: "string"
    }
  } };

  Object.keys(resourceConfig.attributes).forEach(function(attributeName) {
    if (attributeName === "type" || attributeName === "id") return;
    var attribute = resourceConfig.attributes[attributeName];

    if (attribute._settings) {
      mapping.properties[attributeName] = {
        properties: {
          id: {
            type: "string"
          },
          type: {
            type: "string",
            index: "not_analyzed"
          }
        }
      };
    } else {
      var type = attribute._type;
      if (type === "object") return;
      if (type === "number") type = "integer";
      mapping.properties[attributeName] = {
        type: type,
        fields: {
          raw: {
            type: type,
            index: "not_analyzed"
          }
        }
      };
    }
  });

  var result = { };
  result[resourceConfig.resource] = mapping;
  return result;
};


/**
  initialise gets invoked once for each resource that uses this hander.
 */
ElasticsearchStore.prototype.initialise = function(resourceConfig) {
  var self = this;
  var clientConfig = {
    host: "http://localhost:9200"
  };
  var client = new elasticsearch.Client(JSON.parse(JSON.stringify(clientConfig)));
  if (!client) {
    console.error("error connecting to Elasticsearch");
  } else {
    self._db = client;
  }
  self.resourceConfig = resourceConfig;
  self.ready = true;
};

ElasticsearchStore.prototype.populate = function(callback) {
  var self = this;
  self._db.indices.delete({ index: "jsonapi" }, function(err) {
    if (err) console.log("Error dropping index?", err.message);

    self._db.indices.create({ index: "jsonapi" }, function(err1) {
      if (err1) console.log("Error creating index?", err1);

      var mappingRequest = {
        index: "jsonapi",
        type: self.resourceConfig.resource,
        body: self._generateMappingFor(self.resourceConfig)
      };
      self._db.indices.putMapping(mappingRequest, function(err2) {
        if (err2) console.log("Error adding mapping?", err2);

        async.each(self.resourceConfig.examples, function(document, cb) {
          self.create({ params: { type: document.type } }, document, cb);
        }, function(error) {
          if (error) console.error("error creating example resource");
          return callback();
        });
      });
    });
  });
  return self;
};

/**
  Create (store) a new resource give a resource type and an object.
 */
ElasticsearchStore.prototype.create = function(request, newResource, callback) {
  var self = this;
  self._db.index({
    index: "jsonapi",
    type: newResource.type,
    id: newResource.id,
    body: newResource,
    refresh: true
  }, function(err) {
    if (err) {
      return callback({
        status: "404",
        code: "ENOTFOUND",
        title: "Requested resource could not be created",
        detail: "Failed to create " + request.params.type + " with id " + newResource.id
      });
    }
    return callback(null, newResource);
  });
};

/**
  Find a specific resource, given a resource type and and id.
 */
ElasticsearchStore.prototype.find = function(request, callback) {
  var self = this;
  self._db.get({
    index: "jsonapi",
    type: request.params.type,
    id: request.params.id
  }, function(err, theResource) {
    if (err || !theResource) {
      return callback({
        status: "404",
        code: "ENOTFOUND",
        title: "Requested resource does not exist",
        detail: "There is no " + request.params.type + " with id " + request.params.id
      });
    }

    debug("find", JSON.stringify(theResource._source));
    return callback(null, theResource._source);
  });
};

/**
  Search for a list of resources, give a resource type.
 */
ElasticsearchStore.prototype.search = function(request, callback) {
  var self = this;
  var params = _.assign({
    index: "jsonapi",
    q: self._buildQuery(request)
  }, self._applyPagination(request), self._applySort(request));

  debug("search", JSON.stringify(params));
  async.parallel({
    resultSet: function(asyncCallback) {
      self._db.search(params, asyncCallback);
    },
    totalRows: function(asyncCallback) {
      self._db.count(params, asyncCallback);
    }
  }, function(err, results) {
    if (err) {
      debug("err", err);
      return callback(err);
    }

    var totalRows = results.totalRows[0].count;

    results = results.resultSet[0].hits.hits;
    if(!(results instanceof Array)) results = [results];
    results = results.map(function(result) {
      return result._source;
    });

    debug("search", JSON.stringify(results));
    return callback(err, results, totalRows);
  });
};

/**
  Update a resource, given a resource type and id, along with a partialResource.
  partialResource contains a subset of changes that need to be merged over the original.
 */
ElasticsearchStore.prototype.update = function(request, partialResource, callback) {
  var self = this;
  self.find(request, function(err, theResource) {
    if (err) {
      return callback({
        status: "404",
        code: "ENOTFOUND",
        title: "Requested resource could not be updated",
        detail: "There is no " + request.params.type + " with id " + request.params.id
      });
    }

    theResource = _.assign(theResource, partialResource);
    self.create(request, theResource, function(err2) {
      if (err2) {
        return callback({
          status: "404",
          code: "ENOTFOUND",
          title: "Requested resource could not be updated",
          detail: "There is no " + request.params.type + " with id " + request.params.id
        });
      }
      return self.find(request, callback);
    });
  });
};

/**
  Delete a resource, given a resource type and and id.
 */
ElasticsearchStore.prototype.delete = function(request, callback) {
  var self = this;
  self._db.delete({
    index: "jsonapi",
    type: request.params.type,
    id: request.params.id
  }, function(err, response) {
    if (err) {
      return callback({
        status: "404",
        code: "ENOTFOUND",
        title: "Requested resource could not be deleted",
        detail: "There is no " + request.params.type + " with id " + request.params.id
      });
    }
    return callback(null, response);
  });
};