Skip to content

Commit

Permalink
Merge pull request #52 from mozilla-services/list-filtering
Browse files Browse the repository at this point in the history
Closes #6 - Local collection data ordering & filtering.
  • Loading branch information
n1k0 committed Jul 7, 2015
2 parents c0834f5 + 04b0fd4 commit 627bd2e
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 33 deletions.
16 changes: 12 additions & 4 deletions src/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { v4 as uuid4 } from "uuid";
import deepEquals from "deep-eql";

import { attachFakeIDBSymbolsTo } from "./utils";
import { attachFakeIDBSymbolsTo, reduceRecords } from "./utils";
import { cleanRecord } from "./api";

attachFakeIDBSymbolsTo(typeof global === "object" ? global : window);
Expand Down Expand Up @@ -320,11 +320,19 @@ export default class Collection {
/**
* Lists records from the local database.
*
* @param {Object} params
* @param {Object} options
* Params:
* - {Object} filters The filters to apply (default: {}).
* - {String} order The order to apply (default: "-last_modified").
*
* Options:
* - {Boolean} includeDeleted: Include virtually deleted records.
*
* @param {Object} params The filters and order to apply to the results.
* @param {Object} options The options object.
* @return {Promise}
*/
list(params={}, options={includeDeleted: false}) {
params = Object.assign({order: "-last_modified", filters: {}}, params);
return this.open().then(() => {
return new Promise((resolve, reject) => {
const results = [];
Expand All @@ -342,7 +350,7 @@ export default class Collection {
transaction.onerror = event => reject(new Error(event.target.error));
transaction.oncomplete = event => {
resolve({
data: results,
data: reduceRecords(params.filters, params.order, results),
permissions: {}
});
};
Expand Down
55 changes: 55 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,58 @@ export function quote(str) {
export function unquote(str) {
return str.replace(/^"/, "").replace(/"$/, "");
}

/**
* Checks if a value is undefined.
* @param {Any} value
* @return {Boolean}
*/
function _isUndefined(value) {
return typeof value === "undefined";
}

/**
* Sorts records in a list according to a given ordering.
* @param {String} ordering The ordering.
* @param {Array} list The collection to order.
* @return {Array}
*/
export function sortObjects(order, list) {
const hasDash = order[0] === "-";
const field = hasDash ? order.slice(1) : order;
const direction = hasDash ? -1 : 1;
return list.slice().sort((a, b) => {
if (a[field] && _isUndefined(b[field]))
return direction;
if (b[field] && _isUndefined(a[field]))
return -direction;
if (_isUndefined(a[field]) && _isUndefined(b[field]))
return 0;
return a[field] > b[field] ? direction : -direction;
});
}

/**
* Filters records in a list matching all given filters.
* @param {String} filters The filters object.
* @param {Array} list The collection to order.
* @return {Array}
*/
export function filterObjects(filters, list) {
return list.filter(entry => {
return Object.keys(filters).every(filter => {
return entry[filter] === filters[filter];
});
});
}

/**
* Filter and sort list against provided filters and order.
* @param {Object} filters The filters to apply.
* @param {String} order The order to apply.
* @param {Array} list The list to reduce.
* @return {Array}
*/
export function reduceRecords(filters, order, list) {
return sortObjects(order, filterObjects(filters, list));
}
159 changes: 131 additions & 28 deletions test/collection_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import sinon from "sinon";
import { v4 as uuid4 } from "uuid";

import Collection, { SyncResultObject } from "../src/collection";
import Api from "../src/api";
import Api, { cleanRecord } from "../src/api";

chai.use(chaiAsPromised);
chai.should();
Expand Down Expand Up @@ -184,7 +184,7 @@ describe("Collection", () => {
.should.become(article.title);
});

it("should support the forceUUID option", function() {
it("should support the forceUUID option", () => {
return articles.create({id: 42, title: "foo"}, {forceUUID: true})
.then(result => articles.get(result.data.id))
.then(res => res.data.id)
Expand Down Expand Up @@ -391,39 +391,142 @@ describe("Collection", () => {
describe("#list", () => {
var articles;

beforeEach(() => {
articles = testCollection();
return Promise.all([
articles.create(article),
articles.create({title: "bar", url: "http://bar"})
]);
});
describe("Basic", () => {
beforeEach(() => {
articles = testCollection();
return Promise.all([
articles.create(article),
articles.create({title: "bar", url: "http://bar"})
]);
});

it("should retrieve the list of records", () => {
return articles.list()
.then(res => res.data)
.should.eventually.have.length.of(2);
it("should retrieve the list of records", () => {
return articles.list()
.then(res => res.data)
.should.eventually.have.length.of(2);
});

it("shouldn't list virtually deleted records", () => {
return articles.create({title: "yay"})
.then(res => articles.delete(res.data.id))
.then(_ => articles.list())
.then(res => res.data)
.should.eventually.have.length.of(2);
});

it("should support the includeDeleted option", () => {
return articles.create({title: "yay"})
.then(res => articles.delete(res.data.id))
.then(_ => articles.list({}, {includeDeleted: true}))
.then(res => res.data)
.should.eventually.have.length.of(3);
});

it("should prefix error encountered", () => {
sandbox.stub(articles, "open").returns(Promise.reject("error"));
return articles.list().should.be.rejectedWith(Error, /^list/);
});
});

it("shouldn't list virtually deleted records", () => {
return articles.create({title: "yay"})
.then(res => articles.delete(res.data.id))
.then(_ => articles.list())
.then(res => res.data)
.should.eventually.have.length.of(2);
describe("Ordering", () => {
const fixtures = [
{title: "art1", last_modified: 2, unread: false},
{title: "art2", last_modified: 3, unread: true},
{title: "art3", last_modified: 1, unread: false},
];

beforeEach(() => {
articles = testCollection();
return Promise.all(fixtures.map(r => articles.create(r)));
});

it("should order records on last_modified DESC by default", () => {
return articles.list()
.then(res => res.data.map(r => r.title))
.should.eventually.become(["art2", "art1", "art3"]);
});

it("should order records on custom field ASC", () => {
return articles.list({order: "title"})
.then(res => res.data.map(r => r.title))
.should.eventually.become(["art1", "art2", "art3"]);
});

it("should order records on custom field DESC", () => {
return articles.list({order: "-title"})
.then(res => res.data.map(r => r.title))
.should.eventually.become(["art3", "art2", "art1"]);
});

it("should order records on boolean values ASC", () => {
return articles.list({order: "unread"})
.then(res => res.data.map(r => r.unread))
.should.eventually.become([false, false, true]);
});

it("should order records on boolean values DESC", () => {
return articles.list({order: "-unread"})
.then(res => res.data.map(r => r.unread))
.should.eventually.become([true, false, false]);
});
});

it("should support the includeDeleted option", () => {
return articles.create({title: "yay"})
.then(res => articles.delete(res.data.id))
.then(_ => articles.list({}, {includeDeleted: true}))
.then(res => res.data)
.should.eventually.have.length.of(3);
describe("Filtering", () => {
const fixtures = [
{title: "art1", last_modified: 3, unread: true, complete: true},
{title: "art2", last_modified: 2, unread: false, complete: true},
{title: "art3", last_modified: 1, unread: true, complete: false},
];

beforeEach(() => {
articles = testCollection();
return Promise.all(fixtures.map(r => articles.create(r)));
});

it("should filter records on existing field", () => {
return articles.list({filters: {unread: true}})
.then(res => res.data.map(r => r.title))
.should.eventually.become(["art1", "art3"]);
});

it("should filter records on missing field", () => {
return articles.list({filters: {missing: true}})
.then(res => res.data.map(r => r.title))
.should.eventually.become([]);
});

it("should filter records on multiple fields", () => {
return articles.list({filters: {unread: true, complete: true}})
.then(res => res.data.map(r => r.title))
.should.eventually.become(["art1"]);
});
});

it("should prefix error encountered", () => {
sandbox.stub(articles, "open").returns(Promise.reject("error"));
return articles.list().should.be.rejectedWith(Error, /^list/);
describe("Ordering & Filtering", () => {
const fixtures = [
{title: "art1", last_modified: 3, unread: true, complete: true},
{title: "art2", last_modified: 2, unread: false, complete: true},
{title: "art3", last_modified: 1, unread: true, complete: true},
];

beforeEach(() => {
articles = testCollection();
return Promise.all(fixtures.map(r => articles.create(r)));
});

it("should order and filter records", () => {
return articles.list({
order: "-title",
filters: {unread: true, complete: true}
})
.then(res => res.data.map(r => {
return {title: r.title, unread: r.unread, complete: r.complete};
}))
.should.eventually.become([
{title: "art3", unread: true, complete: true},
{title: "art1", unread: true, complete: true},
]);
});
});
});

Expand Down

0 comments on commit 627bd2e

Please sign in to comment.