Last active
August 31, 2021 19:12
-
-
Save avoidwork/ac4024bf90cbadabaaff45ecbc2a18a8 to your computer and use it in GitHub Desktop.
Azure Function node.js with pagination, sorting & hypermedia
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const url = require('url'), | |
config = require('./config.json'), | |
mongodb = require('mongodb'), | |
keysort = require('keysort'); | |
function clone (arg) { | |
return JSON.parse(JSON.stringify(arg)); | |
} | |
function connect (uri) { | |
return new Promise((resolve, reject) => { | |
mongodb.connect(uri, (err, arg) => { | |
if (err) { | |
reject(err); | |
} else { | |
resolve(arg); | |
} | |
}); | |
}); | |
} | |
function find (db, arg) { | |
return new Promise((resolve, reject) => { | |
const coll = db.collection(arg); | |
coll.find({}).toArray((err, args) => { | |
if (err !== null) { | |
reject(err); | |
} else { | |
resolve(args.map(i => { | |
const o = clone(i); | |
delete o._id; // Azure serializer breaks on this key/value | |
return o; | |
})); | |
} | |
}); | |
}); | |
} | |
function keys (query) { | |
const result = {}; | |
query.split('&').forEach(i => { | |
const split = i.split('='); | |
if (split.length > 0 && split[0] !== '') { | |
result[split[0]] = decodeURIComponent(split[1] || ''); | |
} | |
}); | |
return result; | |
} | |
function pages (uri, links, page, page_size, total) { | |
const nth = Math.ceil(total / page_size); | |
if (nth > 1) { | |
if (page > 1) { | |
links.push({uri: uri.replace('page=0', 'page=1'), rel: 'first'}); | |
} | |
if (page - 1 > 1 && page <= nth) { | |
links.push({uri: uri.replace('page=0', `page=${(page - 1)}`), rel: 'prev'}); | |
} | |
if (page + 1 < nth) { | |
links.push({uri: uri.replace('page=0', `page=${(page + 1)}`), rel: 'next'}); | |
} | |
if (nth > 0 && page !== nth) { | |
links.push({uri: uri.replace('page=0', `page=${nth}`), rel: 'last'}); | |
} | |
} | |
} | |
function where (data = [], predicate = {}) { | |
const keys = Object.keys(predicate), | |
signature = keys.length === 0 ? 'return true;' : 'return (' + Object.keys(predicate).map(i => { | |
const arg = typeof predicate[i] === 'string' ? '"' + predicate[i] + '"' : predicate[i]; | |
return 'Array.isArray(a["' + i + '"]) ? a["' + i + '"].includes(' + arg + ') : a["' + i + '"] === ' + arg; | |
}).join(') && (') + ');', | |
fn = new Function('a', signature); | |
return data.filter(fn); | |
} | |
function transform (context, data = [], status = 200) { | |
try { | |
const parsed = url.parse(context.req.originalUrl), | |
links = [{rel: 'collection', uri: '/api'}], | |
query = keys(parsed.query || ''), | |
sort = query.sort || '', | |
page = isNaN(query.page) === false ? Number(query.page) : 1, | |
page_size = isNaN(query.page_size) === false ? Number(query.page_size) : 5, | |
start = (page - 1) * page_size, | |
end = (start > -1 ? start : 0) + page_size; | |
if (query.page_size === void 0) { | |
query.page_size = page_size; | |
} | |
const uri = `${parsed.pathname}?${Object.keys(query).filter(i => i !== 'page').map(i => `${i}=${encodeURIComponent(query[i])}`).join('&')}&page=0`; | |
// Reducing `query` to a predicate | |
delete query.sort; | |
delete query.page; | |
delete query.page_size; | |
data = where(data, query); | |
pages(uri, links, page, page_size, data.length); | |
context.res = { | |
status: status, | |
headers: { | |
'Content-Type': 'application/json', | |
'Cache-Control': 'public, max-age=1800', | |
Links: links.map(i => `<${i.uri}>; rel="${i.rel}"`).join(', ') | |
}, | |
body: { | |
data: (sort !== '' ? keysort(data, sort) : data).slice(start, end), | |
error: null, | |
links: links, | |
status: status | |
} | |
}; | |
} catch (err) { | |
context.res = { | |
status: 500, | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: { | |
data: null, | |
error: err.stack || err.message || err, | |
links: [], | |
status: 500 | |
} | |
}; | |
} | |
} | |
const fn = (context = {req: {originalUrl: '/api/articles?sort=Date%20desc&Solution=Campaign'}, res: {}}) => { | |
let client; | |
connect(config.cosmos.uri).then(arg => { | |
client = arg; | |
}) | |
.then(() => find(client.db(config.cosmos.database), config.cosmos.collection)) | |
.then(data => transform(context, data)) | |
.then(() => { | |
client.close(); | |
client = void 0; | |
context.done(); | |
}) | |
.catch(err => { | |
context.log(err.stack || err.message || err); | |
if (client !== void 0) { | |
try { | |
client.close(); | |
} catch (e) { | |
context.log(e.stack || e.message || e); | |
} | |
} | |
}); | |
}; | |
module.exports = fn; | |
//fn(); // Enable for local dev |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment