Skip to content

Commit

Permalink
Merged pagination with adjustments for optional case sensitivity (in …
Browse files Browse the repository at this point in the history
…query properties only, not yet query keys).
  • Loading branch information
pfyvie committed Nov 14, 2016
2 parents dd462a1 + 284b19b commit 7a660bf
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 22 deletions.
165 changes: 144 additions & 21 deletions lib/operations/collection/get.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,167 @@
'use strict';

function createList(map, context) {
const keys = Object.keys(map);
const query = context.query;
const result = [];
const PAGE_AFTER = 'pageAfter';
const PAGE_NUM = 'pageNum';
const PAGE_SIZE = 'pageSize';
const ORDER_BY = 'orderBy';

for (let i = 0; i < keys.length; i++) {
const id = keys[i];
const resource = map[id];
function getProperties(query) {
const properties = {
matchOn: {}
};
for (const key in query) {
if (key === PAGE_SIZE) {
properties.pageSize = parseInt(query[PAGE_SIZE], 10);
} else if (key === PAGE_NUM) {
properties.pageNum = parseInt(query[PAGE_NUM], 10);
} else if (key === PAGE_AFTER) {
properties.pageAfter = query[PAGE_AFTER];
} else if (key === ORDER_BY) {
properties.orderBy = query[ORDER_BY];
} else {
properties.matchOn[key] = query[key];
}
}
return properties;
}

function getOrderByFound(map, properties) {
let retval = true;
// Check if the property we are ordering by exists
for (const key in map) {
if (map.hasOwnProperty(key)) {
const orderByFound = map[key][properties.orderBy];
if (!orderByFound) {
retval = false;
}
break; // We're only going to check the first element in the map
}
}
return retval;
}

function validateProperties(map, properties) {
let status = 200;

// Validate pagination
const hasPageSize = properties.hasOwnProperty('pageSize');
const pageSizeIsNumber = Number.isInteger(properties.pageSize);
const pageSizeIsValidValue = properties.pageSize > 0 && properties.pageSize === parseInt(properties.pageSize, 10);
const hasPageAfter = properties.hasOwnProperty('pageAfter');
const pageAfterKeyFound = map[properties.pageAfter];
const hasPageNum = properties.hasOwnProperty('pageNum');
const pageNumIsNumber = Number.isInteger(properties.pageNum);
const pageNumIsValidValue = properties.pageNum > 0 && properties.pageNum === parseInt(properties.pageNum, 10);

if (context.mayRead(resource) && (!query || (resource.matches && resource.matches(query)))) {
result.push(resource);
if (hasPageSize) {
if (!pageSizeIsNumber) {
status = 400; // Bad Request: Not a number
} else if (!pageSizeIsValidValue) {
status = 400; // Bad Request: Invalid number
} else if (hasPageAfter && !pageAfterKeyFound) {
status = 404; // Not Found: pageAfter not found
} else if (hasPageNum && !pageNumIsNumber) {
status = 400; // Bad Request: Not a number
} else if (hasPageNum && !pageNumIsValidValue) {
status = 400; // Bad Request: Invalid number
}
} else if (hasPageAfter || hasPageNum) {
status = 400; // Bad Request: Missing pageSize
}

return result;
// Validate Order By
const hasOrderBy = properties.hasOwnProperty('orderBy');
const orderByFound = getOrderByFound(map, properties);

if (hasOrderBy && !orderByFound) {
status = 404; // Not Found: orderBy not found
}

return status;
}

function createMap(map, context) {
function getPagination (context, keys, properties) {
const pagination = {
startIndex: 0,
endIndex: keys.length
};

if (typeof (properties.pageSize) !== 'undefined') {
if (typeof (properties.pageAfter) !== 'undefined') {
pagination.startIndex = keys.indexOf(properties.pageAfter) + 1;
} else if (typeof (properties.pageNum) !== 'undefined') {
pagination.startIndex = (properties.pageNum - 1) * properties.pageSize;
}

// Only use the calculated page end index if it's smaller than the number of keys
const pageEndIndex = pagination.startIndex + properties.pageSize;
if (pagination.endIndex > pageEndIndex) {
pagination.endIndex = pageEndIndex;
}
}
return pagination;
}

function createResult(map, context, properties, resultMap, resultList) {
const keys = Object.keys(map);
const query = context.query;
const result = {};

for (let i = 0; i < keys.length; i++) {
const pagination = getPagination(context, keys, properties);

if (properties.hasOwnProperty('orderBy')) {
// Sort the keys based on the value of property defined in 'orderBy'
keys.sort(function (a, b) {
const valA = map[a][properties.orderBy].toString().toUpperCase();
const valB = map[b][properties.orderBy].toString().toLocaleUpperCase();
if (valA < valB) {
return -1;
}
if (valA > valB) {
return 1;
}
return 0;
});
} else {
// By default sort by the id
keys.sort();
}

const hasNoProps = Object.keys(properties.matchOn).length === 0;
for (let i = pagination.startIndex; i < pagination.endIndex; i++) {
const id = keys[i];
const resource = map[id];

if (context.mayRead(resource) && (!query || (resource.matches && resource.matches(query)))) {
result[id] = resource;
if (context.mayRead(resource) && (hasNoProps || (resource.matches && resource.matches(properties.matchOn)))) {
if (resultMap) {
resultMap[id] = resource;
}
if (resultList) {
resultList.push(resource);
}
}
}

return result;
}


module.exports = function (collection, context, cb) {
const fn = context.createCustomFn('get', collection.Class);
const map = collection.getMapRef();

if (!collection.getCaseSensitive()) {
for (const clause in context.query) {
if (context.query.hasOwnProperty(clause)) {
context.query[clause] = context.query[clause].toLowerCase();
}
}
}
const properties = getProperties(context.query);
const propertyStatus = validateProperties(map, properties);
if (propertyStatus >= 400) {
return cb(context.setStatus(propertyStatus)); // Invalid properties in query string
}

if (fn) {
return fn(createList(map, context));
const resultList = [];
createResult(map, context, properties, null, resultList);
return fn(resultList);
}

if (!context.isJson()) {
Expand All @@ -51,5 +172,7 @@ module.exports = function (collection, context, cb) {
return cb(context.setStatus(304)); // Not Modified
}

cb(context.setStatus(200).setBody(createMap(map, context)));
const resultMap = {};
createResult(map, context, properties, resultMap, null);
cb(context.setStatus(200).setBody(resultMap));
};
128 changes: 127 additions & 1 deletion test/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const createServer = require('./helpers/server');
const basename = require('path').basename;
const rested = require('..');


test('Methods', function (t) {
const options = { rights: true, caseSensitive: false };

Expand Down Expand Up @@ -52,6 +51,27 @@ test('Methods', function (t) {
demolen
};

const allOrderedByRating = {
heineken,
suntorypremium: suntory,
rochefort,
demolen
};

const pageOne = {
demolen,
heineken
};

const pageTwo = {
rochefort,
suntorypremium: suntory
};

const lastPageCutOff = {
suntorypremium: suntory
};

const enMatchers = {
demolen,
heineken
Expand Down Expand Up @@ -421,6 +441,112 @@ test('Methods', function (t) {
});
});

t.test('GET /rest/beer?orderBy=rating (order by rating)', function (t) {
http.get(t, '/rest/beer?orderBy=rating', function (data, res) {
t.equal(res.statusCode, 200, 'HTTP status 200 (OK)');
const dataKeys = Object.keys(data);
const orderedKeys = Object.keys(allOrderedByRating);
t.deepEqual(dataKeys[0], orderedKeys[0], 'First elemenst is Heineken');
t.deepEqual(dataKeys[3], orderedKeys[3], 'Fourth element is Demolen');
t.end();
});
});

t.test('GET /rest/beer?orderBy=badParam (order by not found)', function (t) {
http.get(t, '/rest/beer?orderBy=badParam', function (data, res) {
t.equal(res.statusCode, 404, 'HTTP status 404 (Not Found)');
t.end();
});
});

t.test('GET /rest/beer?pageSize=2 (paginate: default)', function (t) {
http.get(t, '/rest/beer?pageSize=2', function (data, res) {
t.equal(res.statusCode, 200, 'HTTP status 200 (OK)');
t.deepEqual(data, pageOne, 'DeMolen and Heineken returned');
t.end();
});
});

t.test('GET /rest/beer?pageSize=0 (paginate: page size too small)', function (t) {
http.get(t, '/rest/beer?pageSize=0', function (data, res) {
t.equal(res.statusCode, 400, 'HTTP status 400 (Bad Request)');
t.end();
});
});

t.test('GET /rest/beer?pageSize=abc (paginate: page size not number)', function (t) {
http.get(t, '/rest/beer?pageSize=abc', function (data, res) {
t.equal(res.statusCode, 400, 'HTTP status 400 (Bad Request)');
t.end();
});
});

t.test('GET /rest/beer?pageNum=1&pageSize=2 (paginate: page 1)', function (t) {
http.get(t, '/rest/beer?pageNum=1&pageSize=2', function (data, res) {
t.equal(res.statusCode, 200, 'HTTP status 200 (OK)');
t.deepEqual(data, pageOne, 'DeMolen and Heineken returned');
t.end();
});
});

t.test('GET /rest/beer?pageNum=2&pageSize=2 (paginate: page 2)', function (t) {
http.get(t, '/rest/beer?pageNum=2&pageSize=2', function (data, res) {
t.equal(res.statusCode, 200, 'HTTP status 200 (OK)');
t.deepEqual(data, pageTwo, 'Rochefort and Suntory returned');
t.end();
});
});

t.test('GET /rest/beer?pageNum=0&pageSize=2 (paginate: page below minimum)', function (t) {
http.get(t, '/rest/beer?pageNum=0&pageSize=2', function (data, res) {
t.equal(res.statusCode, 400, 'HTTP status 400 (Bad Request)');
t.end();
});
});

t.test('GET /rest/beer?pageNum=abc&pageSize=2 (paginate: page not number)', function (t) {
http.get(t, '/rest/beer?pageNum=abc&pageSize=2', function (data, res) {
t.equal(res.statusCode, 400, 'HTTP status 400 (Bad Request)');
t.end();
});
});

t.test('GET /rest/beer?pageNum=1 (paginate: Has pageNum, Mssing pageSize)', function (t) {
http.get(t, '/rest/beer?pageNum=1', function (data, res) {
t.equal(res.statusCode, 400, 'HTTP status 400 (Bad Request)');
t.end();
});
});

t.test('GET /rest/beer?pageNum=1 (paginate: Has pageAfter, Missing pageSize)', function (t) {
http.get(t, '/rest/beer?pageAfter=SuntoryPremium', function (data, res) {
t.equal(res.statusCode, 400, 'HTTP status 400 (Bad Request)');
t.end();
});
});

t.test('GET /rest/beer?pageAfter=Rochefort&pageSize=2 (paginate: page after rochefort)', function (t) {
http.get(t, '/rest/beer?pageAfter=Rochefort&pageSize=2', function (data, res) {
t.equal(res.statusCode, 200, 'HTTP status 200 (OK)');
t.deepEqual(data, lastPageCutOff, 'Rochefort and Suntory returned');
t.end();
});
});

t.test('GET /rest/beer?pageAfter=Missing&pageSize=2 (paginate: page after not found)', function (t) {
http.get(t, '/rest/beer?pageAfter=Missing&pageSize=2', function (data, res) {
t.equal(res.statusCode, 404, 'HTTP status 404 (Not Found)');
t.end();
});
});

t.test('GET /rest/beer?pageNum=2&pageSize=3 (paginate: last page cut off)', function (t) {
http.get(t, '/rest/beer?pageNum=2&pageSize=3', function (data, res) {
t.equal(res.statusCode, 200, 'HTTP status 200 (OK)');
t.deepEqual(data, lastPageCutOff, 'Suntory returned');
t.end();
});
});

t.test('PUT /rest/beer', function (t) {
http.put(t, '/rest/beer', allButSuntory, function (data, res) {
Expand Down

0 comments on commit 7a660bf

Please sign in to comment.