Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions __tests__/rest.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import rest from "../rest.js"

describe("rest.getPagination", () => {
const originalMaxLimit = process.env.RERUM_MAX_QUERY_LIMIT
const originalMaxSkip = process.env.RERUM_MAX_QUERY_SKIP

afterEach(() => {
process.env.RERUM_MAX_QUERY_LIMIT = originalMaxLimit
process.env.RERUM_MAX_QUERY_SKIP = originalMaxSkip
})

it("returns defaults when values are omitted", () => {
const pagination = rest.getPagination({}, 10)
expect(pagination).toEqual({ limit: 10, skip: 0 })
})

it("accepts explicit non-negative integer values", () => {
const pagination = rest.getPagination({ limit: "25", skip: "5" }, 10)
expect(pagination).toEqual({ limit: 25, skip: 5 })
})

it("rejects malformed values with a 400 status", () => {
expect(() => rest.getPagination({ limit: "12abc", skip: "0" }, 10)).toThrow("`limit` and `skip` values must be non-negative integers or omitted.")
expect(() => rest.getPagination({ limit: "1", skip: "-2" }, 10)).toThrow("`limit` and `skip` values must be non-negative integers or omitted.")
expect(() => rest.getPagination({ limit: "1.5", skip: "0" }, 10)).toThrow("`limit` and `skip` values must be non-negative integers or omitted.")
try {
rest.getPagination({ limit: "hello", skip: "0" }, 10)
} catch (err) {
expect(err.status).toBe(400)
}
})

it("caps values to configured maximums", () => {
process.env.RERUM_MAX_QUERY_LIMIT = "50"
process.env.RERUM_MAX_QUERY_SKIP = "100"
const pagination = rest.getPagination({ limit: "1000", skip: "5000" }, 10)
expect(pagination).toEqual({ limit: 50, skip: 100 })
})
})
28 changes: 2 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 43 additions & 1 deletion rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,45 @@ const hasMultipleContentTypes = (contentType) => {
})
}

const DEFAULT_MAX_QUERY_LIMIT = 500
const DEFAULT_MAX_QUERY_SKIP = 100000

/**
* Parse and bound query pagination values.
*
* @param {Object} [query={}] - Request query object
* @param {number} [defaultLimit=10] - Default limit when omitted
* @returns {{limit:number, skip:number}}
* @throws {Error} If limit or skip are not non-negative integers
*/
const getPagination = (query = {}, defaultLimit = 10) => {
const maxLimit = Number.parseInt(process.env.RERUM_MAX_QUERY_LIMIT ?? `${DEFAULT_MAX_QUERY_LIMIT}`, 10)
const maxSkip = Number.parseInt(process.env.RERUM_MAX_QUERY_SKIP ?? `${DEFAULT_MAX_QUERY_SKIP}`, 10)
const safeMaxLimit = Number.isFinite(maxLimit) && maxLimit > 0 ? maxLimit : DEFAULT_MAX_QUERY_LIMIT
const safeMaxSkip = Number.isFinite(maxSkip) && maxSkip >= 0 ? maxSkip : DEFAULT_MAX_QUERY_SKIP

const parseNonNegativeInteger = (value, fallback) => {
if (value === undefined || value === null || value === "") return fallback
const normalized = `${value}`.trim()
if (!/^\d+$/.test(normalized)) return null
return Number.parseInt(normalized, 10)
}

const limit = parseNonNegativeInteger(query.limit, defaultLimit)
const skip = parseNonNegativeInteger(query.skip, 0)

if (limit === null || skip === null) {
const err = new Error("`limit` and `skip` values must be non-negative integers or omitted.")
err.status = 400
throw err
}

return {
limit: Math.min(limit, safeMaxLimit),
skip: Math.min(skip, safeMaxSkip)
}
}

/**
* Middleware to verify Content-Type headers for endpoints receiving JSON bodies.
* Responds with a 415 Invalid Media Type for Content-Type headers that are not for JSON bodies.
Expand Down Expand Up @@ -57,4 +96,7 @@ const verifyJsonContentType = function (req, res, next) {
return next(err)
}

export default { verifyJsonContentType }
export default {
getPagination,
verifyJsonContentType
}
2 changes: 1 addition & 1 deletion routes/overwrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ router.put('/', rest.verifyJsonContentType, checkAccessToken, async (req, res, n

const overwriteBody = req.body
// check for @id; any value is valid
if (!(overwriteBody['@id'] ?? overwriteBody.id)) {
if (!(overwriteBody?.['@id'] ?? overwriteBody?.id)) {
const err = new Error("No record id to overwrite! (https://store.rerum.io/API.html#overwrite)")
err.status = 400
throw err
Expand Down
22 changes: 4 additions & 18 deletions routes/query.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import express from "express"
import rest from "../rest.js"
import { fetchRerum } from "../rerum.js"

const router = express.Router()

/* POST a query to the thing. */
router.post('/', rest.verifyJsonContentType, async (req, res, next) => {
const lim = req.query.limit ?? 10
const skip = req.query.skip ?? 0
try {
const { limit, skip } = rest.getPagination(req.query, 10)
// check body for JSON
const queryBody = JSON.stringify(req.body)
// If there is an empty query with [] or {}, we consider that a query for all data,
Expand All @@ -17,14 +17,6 @@ router.post('/', rest.verifyJsonContentType, async (req, res, next) => {
err.status = 400
throw err
}
// check limit and skip for INT
if (isNaN(parseInt(lim) + parseInt(skip))
|| (lim < 0)
|| (skip < 0)) {
const err = new Error("`limit` and `skip` values must be positive integers or omitted.")
err.status = 400
throw err
}
const queryOptions = {
method: 'POST',
body: queryBody,
Expand All @@ -35,8 +27,8 @@ router.post('/', rest.verifyJsonContentType, async (req, res, next) => {
'Content-Type' : "application/json;charset=utf-8"
}
}
const queryURL = `${process.env.RERUM_API_ADDR}query?limit=${lim}&skip=${skip}`
const rerumResponse = await fetch(queryURL, queryOptions)
const queryURL = `${process.env.RERUM_API_ADDR}query?limit=${limit}&skip=${skip}`
const rerumResponse = await fetchRerum(queryURL, queryOptions)
.then(async (resp) => {
if (resp.ok) return resp.json()
// The response from RERUM indicates a failure, likely with a specific code and textual body
Expand All @@ -50,12 +42,6 @@ router.post('/', rest.verifyJsonContentType, async (req, res, next) => {
err.status = 502
throw err
})
.catch(err => {
if (err.status === 502) throw err
const genericRerumNetworkError = new Error(`500: ${queryURL} - A RERUM error occurred`)
genericRerumNetworkError.status = 502
throw genericRerumNetworkError
})
res.status(200).json(rerumResponse)
}
catch (err) {
Expand Down
15 changes: 4 additions & 11 deletions routes/update.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from "express"
import checkAccessToken from "../tokens.js"
import rest from "../rest.js"
import { createRerumNetworkError, fetchRerum } from "../rerum.js"

const router = express.Router()

Expand All @@ -9,7 +10,7 @@ router.put('/', rest.verifyJsonContentType, checkAccessToken, async (req, res, n

try {
// check for @id; any value is valid
if (!(req.body['@id'] ?? req.body.id)) {
if (!(req.body?.['@id'] ?? req.body?.id)) {
const err = new Error("No record id to update! (https://store.rerum.io/API.html#update)")
err.status = 400
throw err
Expand All @@ -27,7 +28,7 @@ router.put('/', rest.verifyJsonContentType, checkAccessToken, async (req, res, n
}
}
const updateURL = `${process.env.RERUM_API_ADDR}update`
const rerumResponse = await fetch(updateURL, updateOptions)
const rerumResponse = await fetchRerum(updateURL, updateOptions)
.then(async (resp) => {
if (resp.ok) return resp.json()
// The response from RERUM indicates a failure, likely with a specific code and textual body
Expand All @@ -41,17 +42,9 @@ router.put('/', rest.verifyJsonContentType, checkAccessToken, async (req, res, n
err.status = 502
throw err
})
.catch(err => {
if (err.status === 502) throw err
const genericRerumNetworkError = new Error(`500: ${updateURL} - A RERUM error occurred`)
genericRerumNetworkError.status = 502
throw genericRerumNetworkError
})
if (!(rerumResponse.id || rerumResponse["@id"])) {
// A 200 with garbled data, call it a fail
const genericRerumNetworkError = new Error(`500: ${updateURL} - A RERUM error occurred`)
genericRerumNetworkError.status = 502
throw genericRerumNetworkError
throw createRerumNetworkError(updateURL)
}
res.setHeader("Location", rerumResponse["@id"] ?? rerumResponse.id)
res.status(200).json(rerumResponse)
Expand Down