-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #194 from dhis2/filtering
feat(filtering): support for filtering
- Loading branch information
Showing
24 changed files
with
1,068 additions
and
114 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
const Joi = require('@hapi/joi') | ||
|
||
/** | ||
* Creates a default function to validating | ||
* and transform results the given definition (a joi schema). | ||
* @param {Joi.Schema} defintion - The joi schema to use | ||
* | ||
* @returns {function(dbResult): []} - A function taking some data (may be an array or object) to be validated by the schema. | ||
* The function throws a ValidationError if mapping fails | ||
*/ | ||
const createDefaultValidator = schema => { | ||
return dbResult => | ||
Array.isArray(dbResult) | ||
? dbResult.map(v => Joi.attempt(v, schema)) | ||
: Joi.attempt(dbResult, schema) | ||
} | ||
|
||
module.exports = { | ||
createDefaultValidator, | ||
} |
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
const Boom = require('@hapi/boom') | ||
const Bounce = require('@hapi/bounce') | ||
const Joi = require('@hapi/joi') | ||
const { Filters } = require('../utils/Filter') | ||
const { parseFilterString } = require('../utils/filterUtils') | ||
|
||
/** | ||
* This plugin hooks into onPrehandler, processing all requests it's enabled for. | ||
* It's designed to be used in conjunction with CustomJoi.js that extends Joi | ||
* with a filter type. The filters are validated through Hapi's validate.query. | ||
* | ||
* The purpose of the plugin is to group all filters in `request.plugins.queryFilter.filters`. | ||
* The value of this property is an instance of Filters. | ||
* | ||
* All options can be overwritten by each route. | ||
* Options: | ||
* enabled - enable the queryFilter | ||
* ignoreKeys - ignore the key. Note that only keys of type Joi.filter() in the validate.query object are used. | ||
* rename - A Joi-schema with .rename() functions that are used to get renames of the keys. | ||
* Can also be a plain Object with keys being the query-name, and the value being the database-name. | ||
* This is used to rename API-facing filter-names to database-column names. | ||
*/ | ||
|
||
const FILTER_TYPE = 'filter' | ||
|
||
const defaultOptions = { | ||
enabled: false, // default disabled for all routes | ||
method: ['GET'], | ||
ignoreKeys: ['paging'], | ||
} | ||
|
||
// Holds Joi-schema descriptions, as these can be quite expensive to generate and this is a hot-path | ||
// cached by path as key | ||
|
||
// validateDescriptions is used to get queryParams with Filter-type | ||
const validateDescriptions = {} | ||
// renameDescriptions is used to rename the filter-Keys to the correct DB-column | ||
const renameDescriptions = {} | ||
|
||
const onPreHandler = function(request, h) { | ||
const routeOptions = request.route.settings.plugins.queryFilter || {} | ||
|
||
const options = { | ||
...this.options, | ||
...routeOptions, | ||
ignoreKeys: this.options.ignoreKeys.concat( | ||
routeOptions.ignoreKeys || [] | ||
), | ||
} | ||
|
||
if ( | ||
!options.enabled || | ||
!options.method.includes(request.method.toUpperCase()) | ||
) { | ||
return h.continue | ||
} | ||
|
||
const routeQueryValidation = request.route.settings.validate.query | ||
const rename = routeOptions.rename | ||
const queryFilterKeys = new Set() | ||
let renameMap = rename | ||
let validateDescription = validateDescriptions[request.path] | ||
let renameDescription = renameDescriptions[request.path] | ||
|
||
if (rename && Joi.isSchema(rename)) { | ||
if (!renameDescription) { | ||
renameDescription = renameDescriptions[ | ||
request.path | ||
] = rename.describe() | ||
} | ||
renameMap = renameDescription.renames.reduce((acc, curr) => { | ||
const { from, to } = curr | ||
acc[from] = to | ||
return acc | ||
}, {}) | ||
} | ||
|
||
if (Joi.isSchema(routeQueryValidation)) { | ||
if (!validateDescription) { | ||
validateDescription = validateDescriptions[ | ||
request.path | ||
] = routeQueryValidation.describe() | ||
} | ||
// only add validations with .filter() | ||
Object.keys(validateDescription.keys).forEach(k => { | ||
const keyDesc = validateDescription.keys[k] | ||
if (keyDesc.type === FILTER_TYPE) { | ||
queryFilterKeys.add(k) | ||
} | ||
}) | ||
} else { | ||
// add all keys if no validation | ||
Object.keys(request.query).forEach(key => { | ||
const val = request.query[key] | ||
const parsed = parseFilterString(val) | ||
request.query[key] = parsed | ||
queryFilterKeys.add(key) | ||
}) | ||
} | ||
|
||
const queryFilters = Object.keys(request.query).reduce((acc, curr) => { | ||
if ( | ||
queryFilterKeys.has(curr) && | ||
options.ignoreKeys.indexOf(curr) === -1 | ||
) { | ||
acc[curr] = request.query[curr] | ||
} | ||
return acc | ||
}, {}) | ||
|
||
try { | ||
const filters = new Filters(queryFilters, { | ||
renameMap, | ||
}) | ||
request.plugins.queryFilter = filters | ||
} catch (e) { | ||
Bounce.rethrow(e, 'system') | ||
throw Boom.boomify(e, { statusCode: 400 }) | ||
} | ||
|
||
return h.continue | ||
} | ||
|
||
const filterPlugin = { | ||
name: 'FilterPlugin', | ||
register: async (server, options) => { | ||
const opts = { | ||
...defaultOptions, | ||
...options, | ||
} | ||
server.bind({ | ||
options: opts, | ||
}) | ||
|
||
server.ext('onPreHandler', onPreHandler) | ||
}, | ||
} | ||
|
||
module.exports = filterPlugin |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
const { flatten } = require('../../utils') | ||
|
||
module.exports = [require('./apps.js'), require('./channels.js'), require('./organisations')] | ||
module.exports = [ | ||
require('./apps.js'), | ||
require('./channels.js'), | ||
require('./organisations'), | ||
] |
Oops, something went wrong.