fortune-http
Version:
HTTP implementation for Fortune.js.
336 lines (261 loc) • 9.52 kB
JavaScript
var urlLib = require('url')
var parseUrl = urlLib.parse
var isMatch = /^match\./
var isRange = /^range\./
var isExists = /^exists\./
var buffer = Buffer.from || Buffer
var allowLevel = [
[ 'GET' ], // Index
[ 'GET', 'POST', 'PATCH', 'DELETE' ], // Collection
[ 'GET', 'PATCH', 'DELETE' ], // Records
[ 'GET', 'PATCH', 'DELETE' ] // Related records
]
var entityMap = {
'-': '+',
'_': '/'
}
// Cache parsed prefix paths.
var prefixPath = {}
function setupFn (instance) {
var common = instance.common
var castToNumber = common.castToNumber
var message = common.message
var methods = common.methods
var assign = common.assign
var castValue = common.castValue
var map = common.map
var filter = common.filter
var reduce = common.reduce
var errors = common.errors
var NotFoundError = errors.NotFoundError
var keys = common.keys
var typeKey = keys.type
var linkKey = keys.link
var denormalizedInverseKey = keys.denormalizedInverse
var methodMap = {
'GET': methods.find,
'POST': methods.create,
'PATCH': methods.update,
'DELETE': methods.delete
}
// Expose internal query attachment function.
initializeContext.attachQueries = attachQueries
return initializeContext
function initializeContext (contextRequest, request, response) {
var recordTypes = this.recordTypes
var adapter = this.adapter
var meta = contextRequest.meta
var options = this.options
var uriBase64 = options.uriBase64
var castId = options.castId
var prefix = options.prefix
var url = request.url
var type, ids, fields, relatedField, language, parsedUrl, parts,
route, query, findOptions, output, i, j
request.meta = {}
language = request.meta.language = meta.language
// Set the request method.
request.meta.method = contextRequest.method =
methodMap[request.method]
// Decode URIs.
if (uriBase64) {
// The query string should not be encoded.
route = url.slice(1).split('?')
query = '?' + route.slice(1).join('?')
url = '/' + buffer((route[0] + Array(5 - route[0].length % 4)
.join('=')).replace(/[\-_]/g, function (x) { return entityMap[x] }),
'base64').toString() + query
}
parsedUrl = contextRequest.parsedUrl = request.meta.parsedUrl =
parseUrl(url, true)
// If a prefix is specified, it is necessary to remove it from the path
// before processing it.
if (prefix) {
if (!prefixPath.hasOwnProperty(prefix))
prefixPath[prefix] = parseUrl(prefix).pathname
prefix = prefixPath[prefix]
if (parsedUrl.pathname.indexOf(prefix) === 0)
parsedUrl.pathname = parsedUrl.pathname.substr(prefix.length)
}
parts = parsedUrl.pathname.split('/')
// Strip empty string before slash prefix.
parts.shift()
// Strip trailing slash.
if (parts[parts.length - 1] === '') parts.pop()
for (i = 0, j = parts.length; i < j; i++)
parts[i] = decodeURIComponent(parts[i])
if (parts.length > 3)
throw new NotFoundError(message('InvalidURL', language))
if (parts[0]) {
type = request.meta.type = contextRequest.type = parsedUrl.type =
parts[0]
if (!recordTypes.hasOwnProperty(type))
throw new NotFoundError(
message('InvalidType', language, { type: type }))
fields = recordTypes[type]
}
else parts.shift()
// Respond to options request.
if (request.method === 'OPTIONS' &&
(!type || recordTypes.hasOwnProperty(type))) {
response.statusCode = 204
output = new Error()
output.isMethodInvalid = true
output.meta = {
headers: {
'Allow': allowLevel[parts.length].join(', ')
}
}
throw output
}
if (parts[1]) {
ids = request.meta.ids = contextRequest.ids = parsedUrl.ids =
parts[1].split(',')
if (castId)
ids = request.meta.ids = contextRequest.ids = map(ids, castToNumber)
}
if (parts[2])
relatedField = contextRequest.relatedField = request.meta.relatedField =
parsedUrl.relatedField = parts[2]
attachQueries.call(this, contextRequest, parsedUrl.query)
request.meta.include = contextRequest.include
request.meta.options = contextRequest.options
if (relatedField) {
if (!fields.hasOwnProperty(relatedField) ||
!(linkKey in fields[relatedField]) ||
fields[relatedField][denormalizedInverseKey])
throw new NotFoundError(message('InvalidURL', language))
// Only care about getting the related field.
findOptions = { fields: {} }
findOptions.fields[relatedField] = true
return adapter.find(type, ids, findOptions, meta)
.then(function (records) {
// Reduce the related IDs from all of the records into an array of
// unique IDs.
var relatedIds = []
var seen = {}
var value, relatedType
var i, j, k, l
for (i = 0, j = records.length; i < j; i++) {
value = records[i][relatedField]
if (!Array.isArray(value)) value = [ value ]
for (k = 0, l = value.length; k < l; k++)
if (!seen.hasOwnProperty(value[k])) {
seen[value[k]] = true
relatedIds.push(value[k])
}
}
relatedType = fields[relatedField][linkKey]
// Copy the original type and IDs to other keys.
contextRequest.originalType = request.meta.originalType = type
contextRequest.originalIds = request.meta.originalIds = ids
// Write the related info to the request, which should take
// precedence over the original type and IDs.
contextRequest.type = request.meta.type = relatedType
contextRequest.ids = request.meta.ids = relatedIds
return contextRequest
})
}
return contextRequest
}
function attachQueries (contextRequest, query) {
var recordTypes = this.recordTypes
var includeLimit = this.options.includeLimit || 5
var maxLimit = this.options.maxLimit || 1000
var options = contextRequest.options = {}
var type = contextRequest.type
var fields = recordTypes[type]
var opts = { language: contextRequest.meta.language }
var parameter, field, fieldType, value, limit
// Attach fields option.
if ('fields' in query) {
options.fields = reduce(
Array.isArray(query.fields) ? query.fields : [ query.fields ],
function (fields, field) {
fields[field] = true
return fields
}, {})
// Remove empty queries.
delete options.fields['']
if (!Object.keys(options.fields).length) delete options.fields
}
// Iterate over dynamic query strings.
for (parameter in query) {
field = parameter.split('.')[1]
// Attach match option.
if (parameter.match(isMatch)) {
value = query[parameter]
if (value === '') continue
if (!options.match) options.match = {}
fieldType = fields[field][typeKey]
options.match[field] = Array.isArray(value) ? map(value,
curryCast(castValue, fieldType, assign(opts, options))) :
castValue(value, fieldType, assign(opts, options))
continue
}
// Attach range option.
if (parameter.match(isRange)) {
value = query[parameter]
if (value === '') continue
if (!options.range) options.range = {}
fieldType = fields[field][typeKey]
if (!Array.isArray(value)) value = [ value ]
options.range[field] = map(value,
curryCast(castValue, fieldType, assign(opts, this.options)))
continue
}
// Attach exists option.
if (parameter.match(isExists)) {
value = query[parameter]
if (value === '') continue
if (!options.exists) options.exists = {}
if (value === '0' || value === 'false')
options.exists[field] = false
if (value === '1' || value === 'true')
options.exists[field] = true
}
}
// Attach sort option.
if ('sort' in query) {
options.sort = reduce(
Array.isArray(query.sort) ? query.sort : [ query.sort ],
function (sort, field) {
if (field.charAt(0) === '-') sort[field.slice(1)] = false
else sort[field] = true
return sort
}, {})
// Remove empty queries.
delete options.sort['']
if (!Object.keys(options.sort).length) delete options.sort
}
// Attach include option.
if ('include' in query)
contextRequest.include = map(
filter(Array.isArray(query.include) ?
query.include : [ query.include ],
function (x) { return x }),
function (x) {
var parts = x.split(',')
var path = parts[0].split('.')
path.splice(includeLimit)
if (parts[1]) path.push(JSON.parse(parts[1]))
return path
})
// Attach offset option.
if ('offset' in query)
options.offset = Math.abs(parseInt(query.offset, 10))
// Attach limit option.
if ('limit' in query)
options.limit = Math.abs(parseInt(query.limit, 10))
// Check limit option.
limit = options.limit
if (!limit || limit > maxLimit) options.limit = maxLimit
}
}
function curryCast (fn, type, options) {
return function (value) {
return fn(value, type, options)
}
}
module.exports = setupFn