diff --git a/Documentation/Books/Users/Foxx/FoxxRepository.mdpp b/Documentation/Books/Users/Foxx/FoxxRepository.mdpp index 6cf0c78318..1e7d8788e7 100644 --- a/Documentation/Books/Users/Foxx/FoxxRepository.mdpp +++ b/Documentation/Books/Users/Foxx/FoxxRepository.mdpp @@ -3,15 +3,15 @@ A repository is a gateway to the database. It gets data from the database, updates it or saves new data. It uses the given model when it returns a model and expects instances of the model for methods like save. In your repository file, export the repository as *repository*. ```javascript -Foxx = require("org/arangodb/foxx"); +var Foxx = require("org/arangodb/foxx"); -TodosRepository = Foxx.Repository.extend({ +var TodosRepository = Foxx.Repository.extend({ }); exports.repository = TodosRepository; ``` -!SUBSECTION Initialize +!SUBSECTION Initialize @startDocuBlock JSF_foxx_repository_initializer @@ -29,6 +29,29 @@ exports.repository = TodosRepository; @startDocuBlock JSF_foxx_repository_prefix +!SECTION Defining indexes + +Repository can take care of ensuring the existence of collection indexes for you. +If you define indexes for a repository, instances of the repository will have +access to additional index-specific methods like *range* or *fulltext* (see below). + +The syntax for defining indexes is the same used in [*collection.ensureIndex*](../IndexHandling/README.md). + +@EXAMPLES + +```js +var Foxx = require('org/arangodb/foxx'); +var FulltextRepository = Foxx.Repository.extend({ + indexes: [ + { + type: 'fulltext', + fields: ['text'], + minLength: 3 + } + ] +}); +``` + !SECTION Methods of a Repository !SUBSECTION Adding entries to the repository @@ -70,3 +93,13 @@ exports.repository = TodosRepository; !SUBSECTION Counting entries in the repository @startDocuBlock JSF_foxx_repository_count + +!SUBSECTION Index-specific repository methods + +@startDocuBlock JSF_foxx_repository_range + +@startDocuBlock JSF_foxx_repository_near + +@startDocuBlock JSF_foxx_repository_within + +@startDocuBlock JSF_foxx_repository_fulltext diff --git a/js/server/modules/org/arangodb/foxx/repository.js b/js/server/modules/org/arangodb/foxx/repository.js index d7fbdb921a..1dd2e3967f 100644 --- a/js/server/modules/org/arangodb/foxx/repository.js +++ b/js/server/modules/org/arangodb/foxx/repository.js @@ -93,6 +93,12 @@ Repository = function (collection, opts) { //////////////////////////////////////////////////////////////////////////////// this.prefix = this.options.prefix; + + if (this.indexes) { + _.each(this.indexes, function (index) { + this.collection.ensureIndex(index); + }, this); + } }; // ----------------------------------------------------------------------------- @@ -106,7 +112,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_save -///`save(model)` +/// `FoxxRepository#save(model)` /// /// Saves a model into the database. /// Expects a model. Will set the ID and Rev on the model. @@ -132,7 +138,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_byId -/// `byId(id)` +/// `FoxxRepository#byId(id)` /// /// Returns the model for the given ID. /// @@ -152,7 +158,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_byExample -/// `byExample(example)` +/// `FoxxRepository#byExample(example)` /// /// Returns an array of models for the given ID. /// @@ -174,7 +180,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_firstExample -/// `firstExample(example)` +/// `FoxxRepository#firstExample(example)` /// /// Returns the first model that matches the given example. /// @@ -194,14 +200,20 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_all -/// `all()` +/// `FoxxRepository#all()` /// -/// Returns an array of models that matches the given example. You need to provide +/// Returns an array of models that matches the given example. You can provide /// both a skip and a limit value. /// /// **Warning:** ArangoDB doesn't guarantee a specific order in this case, to make /// this really useful we have to explicitly provide something to order by. /// +/// *Parameter* +/// +/// * *options* (optional): +/// * *skip* (optional): skips the first given number of models. +/// * *limit* (optional): only returns at most the given number of models. +/// /// @EXAMPLES /// /// ```javascript @@ -212,8 +224,17 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// all: function (options) { 'use strict'; - var rawDocuments = this.collection.all().skip(options.skip).limit(options.limit).toArray(); - return _.map(rawDocuments, function (rawDocument) { + if (!options) { + options = {}; + } + var rawDocuments = this.collection.all(); + if (options.skip) { + rawDocuments = rawDocuments.skip(options.skip); + } + if (options.limit) { + rawDocuments = rawDocuments.limit(options.limit); + } + return _.map(rawDocuments.toArray(), function (rawDocument) { return (new this.modelPrototype(rawDocument)); }, this); }, @@ -224,7 +245,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_remove -/// `remove(model)` +/// `FoxxRepository#remove(model)` /// /// Remove the model from the repository. /// Expects a model. @@ -244,7 +265,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_removeById -/// `removeById(id)` +/// `FoxxRepository#removeById(id)` /// /// Remove the document with the given ID. /// Expects an ID of an existing document. @@ -263,7 +284,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_removeByExample -/// `removeByExample(example)` +/// `FoxxRepository#removeByExample(example)` /// /// Find all documents that fit this example and remove them. /// @@ -285,7 +306,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_replace -/// `replace(model)` +/// `FoxxRepository#replace(model)` /// /// Find the model in the database by its *_id* and replace it with this version. /// Expects a model. Sets the Revision of the model. @@ -310,7 +331,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_replaceById -/// `replaceById(id, model)` +/// `FoxxRepository#replaceById(id, model)` /// /// Find the model in the database by the given ID and replace it with the given. /// model. @@ -333,7 +354,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_replaceByExample -/// `replaceByExample(example, model)` +/// `FoxxRepository#replaceByExample(example, model)` /// /// Find the model in the database by the given example and replace it with the given. /// model. @@ -360,7 +381,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_updateById -/// `updateById(id, object)` +/// `FoxxRepository#updateById(id, object)` /// /// Find an item by ID and update it with the attributes in the provided object. /// Returns the updated model. @@ -379,7 +400,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_updateByExample -/// `updateByExample(example, object)` +/// `FoxxRepository#updateByExample(example, object)` /// /// Find an item by example and update it with the attributes in the provided object. /// Returns the updated model. @@ -402,7 +423,7 @@ _.extend(Repository.prototype, { //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_count -/// `count()` +/// `FoxxRepository#count()` /// /// Returns the number of entries in this collection. /// @@ -419,7 +440,228 @@ _.extend(Repository.prototype, { } }); -Repository.extend = extend; +var indexPrototypes = { + skiplist: { + +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_repository_range +/// `FoxxRepository#range(attribute, left, right)` +/// +/// Returns all models in the repository such that the attribute is greater +/// than or equal to *left* and strictly less than *right*. +/// +/// For range queries it is required that a skiplist index is present for the +/// queried attribute. If no skiplist index is present on the attribute, the +/// method will not be available. +/// +/// *Parameter* +/// +/// * *attribute*: attribute to query. +/// * *left*: lower bound of the value range (inclusive). +/// * *right*: upper bound of the value range (exclusive). +/// +/// @EXAMPLES +/// +/// ```javascript +/// repository.range("age", 10, 13); +/// ``` +/// @endDocuBlock +//////////////////////////////////////////////////////////////////////////////// + range: function (attribute, left, right) { + 'use strict'; + var rawDocuments = this.collection.range(attribute, left, right).toArray(); + return _.map(rawDocuments, function (rawDocument) { + return (new this.modelPrototype(rawDocument)); + }, this); + } + }, + geo: { + +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_repository_near +/// `FoxxRepository#near(latitude, longitude, options)` +/// +/// Finds models near the coordinate *(latitude, longitude)*. The returned +/// list is sorted by distance with the nearest model coming first. +/// +/// For geo queries it is required that a geo index is present in the +/// repository. If no geo index is present, the methods will not be available. +/// +/// *Parameter* +/// +/// * *latitude*: latitude of the coordinate. +/// * *longitude*: longitude of the coordinate. +/// * *options* (optional): +/// * *geo* (optional): name of the specific geo index to use. +/// * *distance* (optional): If set to a truthy value, the returned models +/// will have an additional property containing the distance between the +/// given coordinate and the model. If the value is a string, that value +/// will be used as the property name, otherwise the name defaults to *"distance"*. +/// * *limit* (optional): number of models to return. Defaults to *100*. +/// +/// @EXAMPLES +/// +/// ```javascript +/// repository.near(0, 0, {geo: "home", distance: true, limit: 10}); +/// ``` +/// @endDocuBlock +//////////////////////////////////////////////////////////////////////////////// + near: function (latitude, longitude, options) { + 'use strict'; + var collection = this.collection, + rawDocuments; + if (!options) { + options = {}; + } + if (options.geo) { + collection = collection.geo(options.geo); + } + rawDocuments = collection.near(latitude, longitude); + if (options.distance) { + rawDocuments = rawDocuments.distance(); + } + if (options.limit) { + rawDocuments = rawDocuments.limit(options.limit); + } + return _.map(rawDocuments.toArray(), function (rawDocument) { + var model = (new this.modelPrototype(rawDocument)), + distance; + if (options.distance) { + delete model.attributes._distance; + distance = typeof options.distance === "string" ? options.distance : "distance"; + model[distance] = rawDocument._distance; + } + return model; + }, this); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_repository_within +/// `FoxxRepository#within(latitude, longitude, radius, options)` +/// +/// Finds models within the distance *radius* from the coordinate +/// *(latitude, longitude)*. The returned list is sorted by distance with the +/// nearest model coming first. +/// +/// For geo queries it is required that a geo index is present in the +/// repository. If no geo index is present, the methods will not be available. +/// +/// *Parameter* +/// +/// * *latitude*: latitude of the coordinate. +/// * *longitude*: longitude of the coordinate. +/// * *radius*: maximum distance from the coordinate. +/// * *options* (optional): +/// * *geo* (optional): name of the specific geo index to use. +/// * *distance* (optional): If set to a truthy value, the returned models +/// will have an additional property containing the distance between the +/// given coordinate and the model. If the value is a string, that value +/// will be used as the property name, otherwise the name defaults to *"distance"*. +/// * *limit* (optional): number of models to return. Defaults to *100*. +/// +/// @EXAMPLES +/// +/// ```javascript +/// repository.within(0, 0, 2000 * 1000, {geo: "home", distance: true, limit: 10}); +/// ``` +/// @endDocuBlock +//////////////////////////////////////////////////////////////////////////////// + within: function (latitude, longitude, radius, options) { + 'use strict'; + var collection = this.collection, + rawDocuments; + if (!options) { + options = {}; + } + if (options.geo) { + collection = collection.geo(options.geo); + } + rawDocuments = collection.within(latitude, longitude, radius); + if (options.distance) { + rawDocuments = rawDocuments.distance(); + } + if (options.limit) { + rawDocuments = rawDocuments.limit(options.limit); + } + return _.map(rawDocuments.toArray(), function (rawDocument) { + var model = (new this.modelPrototype(rawDocument)), + distance; + if (options.distance) { + delete model.attributes._distance; + distance = typeof options.distance === "string" ? options.distance : "distance"; + model[distance] = rawDocument._distance; + } + return model; + }, this); + } + }, + fulltext: { + +//////////////////////////////////////////////////////////////////////////////// +/// @startDocuBlock JSF_foxx_repository_fulltext +/// `FoxxRepository#fulltext(attribute, query, options)` +/// +/// Returns all models whose attribute *attribute* matches the search query +/// *query*. +/// +/// In order to use the fulltext method, a fulltext index must be defined on +/// the repository. If multiple fulltext indexes are defined on the repository +/// for the attribute, the most capable one will be selected. +/// If no fulltext index is present, the method will not be available. +/// +/// *Parameter* +/// +/// * *attribute*: model attribute to perform a search on. +/// * *query*: query to match the attribute against. +/// * *options* (optional): +/// * *limit* (optional): number of models to return. Defaults to all. +/// +/// @EXAMPLES +/// +/// ```javascript +/// repository.fulltext("text", "word", {limit: 1}); +/// ``` +/// @endDocuBlock +//////////////////////////////////////////////////////////////////////////////// + fulltext: function (attribute, query, options) { + 'use strict'; + if (!options) { + options = {}; + } + var rawDocuments = this.collection.fulltext(attribute, query); + if (options.limit) { + rawDocuments = rawDocuments.limit(options.limit); + } + return _.map(rawDocuments.toArray(), function (rawDocument) { + return (new this.modelPrototype(rawDocument)); + }, this); + } + } +}; + +var addIndexMethods = function (prototype) { + 'use strict'; + _.each(prototype.indexes, function (index) { + var protoMethods = indexPrototypes[index.type]; + if (!protoMethods) { + return; + } + _.each(protoMethods, function (method, key) { + if (prototype[key] === undefined) { + prototype[key] = method; + } + }); + }); +}; + +Repository.extend = function (prototypeProperties, constructorProperties) { + 'use strict'; + var constructor = extend.call(this, prototypeProperties, constructorProperties); + if (constructor.prototype.hasOwnProperty('indexes')) { + addIndexMethods(constructor.prototype); + } + return constructor; +}; exports.Repository = Repository; diff --git a/js/server/tests/shell-foxx-repository-spec.js b/js/server/tests/shell-foxx-repository-spec.js index 698ab55c30..1943cb6db4 100644 --- a/js/server/tests/shell-foxx-repository-spec.js +++ b/js/server/tests/shell-foxx-repository-spec.js @@ -58,6 +58,63 @@ describe('Repository', function () { }); }); +describe('Repository Indexes', function () { + 'use strict'; + it('should create indexes on instantiation', function () { + var collection = createSpyObj('collection', [ + 'ensureIndex' + ]), + indexes = [ + {type: 'skiplist', xyz: 'abcdef'}, + {type: 'geo', more: 'args'}, + {type: 'foo', bar: 'qux'} + ], + Repository = FoxxRepository.extend({ + indexes: indexes + }); + + new Repository(collection, {model: Model}); + + expect(collection.ensureIndex.calls.count()).toEqual(3); + expect(collection.ensureIndex).toHaveBeenCalledWith(indexes[0]); + expect(collection.ensureIndex).toHaveBeenCalledWith(indexes[1]); + expect(collection.ensureIndex).toHaveBeenCalledWith(indexes[2]); + }); + + it('should add skiplist methods to the prototype', function () { + expect(FoxxRepository.prototype.range).toBeUndefined(); + var Repository = FoxxRepository.extend({ + indexes: [ + {type: 'skiplist'} + ] + }); + + expect(typeof Repository.prototype.range).toBe('function'); + }); + + it('should add geo methods to the prototype', function () { + expect(FoxxRepository.prototype.near).toBeUndefined(); + expect(FoxxRepository.prototype.within).toBeUndefined(); + var Repository = FoxxRepository.extend({ + indexes: [ + {type: 'geo'} + ] + }); + expect(typeof Repository.prototype.near).toBe('function'); + expect(typeof Repository.prototype.within).toBe('function'); + }); + + it('should add fulltext methods to the prototype', function () { + expect(FoxxRepository.prototype.fulltext).toBeUndefined(); + var Repository = FoxxRepository.extend({ + indexes: [ + {type: 'fulltext'} + ] + }); + expect(typeof Repository.prototype.fulltext).toBe('function'); + }); +}); + describe('Repository Methods', function () { 'use strict'; var collection,