Simple Query Parameter Validator using Custom Policies
One of the most powerful aspects of Zuplo is the programmable extensibility. Recently somebody on our Discord channel asked if we supported query parameter validation as we do JSON Body validation.
We plan to add this soon as a built-in policy (which will use your OpenAPI specification). However, I spent 20 minutes building a custom policy to demonstrate how easy it would be to build a custom policy to support this while you wait.
Here's how you would configure the policy
{
"export": "default",
"module": "$import(./modules/query-param-validator)",
"options": {
"allowAdditionalParameters": false,
"params": [
{
"name": "foo",
"required": true,
"type": "int"
},
{
"name": "bar",
"required": true,
"type": "number"
},
{
"name": "wib",
"required": false,
"type": "string"
},
{
"name": "ble",
"required": true,
"type": "boolean"
}
]
}
}
This defines a policy for a route (which can be reused on other routes) that
states there are four supported query parameters: foo
, bar
, wib
and ble
.
No additional query parameters are allowed.
Note that foo
, bar
and ble
are required, whereas wib
is optional.
Each has a different type specified, and the request will be rejected if the
data cannot be parsed as that type from the options int
, number
, string
,
and boolean
.
Here are some hits on that URL and associated error responses (status code 400):
Path: /query
Bad Request
Required query parameter 'foo' missing
Required query parameter 'bar' missing
Required query parameter 'ble' missing
Path: /query?foo=&bar=hey&wib=nope&ble=23
Bad Request
Required query parameter 'foo' missing
Invalid value for query parameter 'bar': 'hey' is not a valid number
Invalid value for query parameter 'ble': '23' not a valid boolean value (expect 'true' or false')
Easy peasy - here's the code for that custom policy
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
type SupportedTyped = "int" | "number" | "string" | "boolean";
type ParameterValidationRule = {
name: string;
required?: boolean;
type?: SupportedTyped;
};
type QueryParamValidatorOptions = {
params: ParameterValidationRule[];
allowAdditionalParameters?: boolean;
};
const typeValidators: Record<
SupportedTyped,
(value: string) => string | undefined
> = {
int: (value: string) => {
const int = parseFloat(value);
if (!Number.isInteger(int)) {
return `'${value}' is not a valid integer`;
}
},
number: (value: string) => {
const float = parseFloat(value);
if (Number.isNaN(float)) {
return `'${value}' is not a valid number`;
}
},
string: (value: string) => {
if (value.length === 0) {
return `empty string provided`;
}
},
boolean: (value: string) => {
if (!["true", "false"].includes(value)) {
return `'${value}' not a valid boolean value (expect 'true' or false')`;
}
},
};
export default async function (
request: ZuploRequest,
context: ZuploContext,
options: QueryParamValidatorOptions,
policyName: string,
) {
const allowAdditionalParameters = options.allowAdditionalParameters ?? false;
const q = request.query;
const errors: string[] = [];
// 1. check no additional parameters
if (!allowAdditionalParameters) {
const allowedNames = options.params.map((p) => p.name);
for (const queryName of Object.keys(q)) {
if (!allowedNames.includes(queryName)) {
errors.push(`Additional query parameter '${queryName}' not allowed`);
}
}
}
// 2. check required and value types
for (const param of options.params) {
const value = q[param.name];
const required = param.required ?? true;
if (!value) {
if (!required) {
continue;
}
// required parameter not provided.
errors.push(`Required query parameter '${param.name}' missing`);
}
if (param.type && value) {
const validatorResult = typeValidators[param.type](value);
if (validatorResult) {
errors.push(
`Invalid value for query parameter '${param.name}': ${validatorResult}`,
);
}
}
}
if (errors.length > 0) {
return new Response(`Bad Request\n\n${errors.join("\n")}`, { status: 400 });
}
return request;
}
Have fun!