How to configure fastify and openapiglue to only coerce query parameters - plugins

Our application is using fastify and we're registering the openapiGlue plugin to parse/validate http requests. We're using the ajv option to disable all type coercion.
Everything works great...
However, the issue with the ajv coerce option is that it is applied globally. What we'd like to do is enable type coercion only for the query parameters, but since the ajv coerce option is global, there's no way to do that.
We've tried to register another plugin, that we wrote, after the openapiGlue that uses the fastify setValidatorCompiler() to solve this problem for us. We've set up separate ajv options sections that get applied based on the part of the http request getting validated. The issue is the setValidatorCompiler callback function never gets invoked for any of the message parts, and thus we are unable to enable type coercion on just the query parameters.
It's as if the openapiGlue plugin doesn't want to relinquish control of the message parsing and validating.
Anyone have any suggestions?
// this works
await app.register(openapiGlue, {
specification: localSystemApi,
service: handlers,
prefix: apiPrefix,
ajvOptions: {
coerceTypes: false // defaults to true by openapiGlue otherwise
}
});
// new code to add plugin to only coerce query parameters
// has no effect.
await app.register(ajvPlugin);
// Our ajvPlugin
const httpPartSchema = z.union([z.literal('body'), z.literal('headers'), z.literal('querystring'), z.literal('params')]);
export const ajvPlugin: FastifyPluginAsync<FastifyOpenapiGlueOptions> = async (fastify) => {
fastify.setValidatorCompiler(({ schema, httpPart }) => {
// this code never gets invoked
if (httpPart === undefined) {
throw new Error('Missing httpPart');
}
const parseResult = httpPartSchema.parse(httpPart);
const compiler = schemaCompilers[parseResult];
return compiler.compile(schema) as any;
});
};

The issue is the setValidatorCompiler callback function never gets invoked for any of the message parts, and thus we are unable to enable type coercion on just the query parameters
This is a feature 😄
Fastify does not create any ajv instances if your routes do not use the schema option to speed up your server startup time.
You can do what you need by adding a route that adds a schema.query object.
I would suggest to use the fastify-split-validator plugin to change the ajv settings based on the HTTP entity.
As further reading, this issue in the fastify repository explains in detail how it works.

Related

Working with URL parameters in custom Kibana plugin

I am working on a custom plugin to Kibana (7.5.2). The plugin is of type 'app'. I would like to be able to pass parameters to this plugin in order to pre-load some data from Elasticsearch. I.e., I need to provide users with some specific URLs containing parameters that will be used by the plugin to show only a relevant portion of data.
My problem is that I was not able to find sufficient documentation on this and I do not know what the correct approach should be. I will try to summarize what I know/have done so far:
I have read the official resources on plugin development
I am aware of the fact that _g and _a URL parameters are used to pass state in Kibana applications. However, a) I am not sure if this is the correct approach in my case and b) I also failed to find any information on how my plugin should access the data from these parameters.
I checked the sources of other known plugins, but again, failed to find any clues.
I am able to inject some configuration values using injectUiAppVars in the init method of my plugin (index.js) and retrieve these values in my app (main.js):
index.js:
export default function (kibana) {
return new kibana.Plugin({
require: ['elasticsearch'],
name: ...,
uiExports: {
...
},
...
init(server, options) { // eslint-disable-line no-unused-vars
server.injectUiAppVars('logviewer', async () => {
var kibana_vars = await server.getInjectedUiAppVars('kibana');
var aggregated_vars = { ...kibana_vars, ...{ mycustomparameter: "some value" } }
return aggregated_vars
});
...
}
});
}
main.js
import chrome from 'ui/chrome';
. . .
const mycustomparameter = chrome.getInjected('mycustomparameter');
Providing that I manage to obtain parameters from URL, this would allow me to pass them to my app (via mycustomparameter), but again, I am not sure if this approach is correct.
I tried to get some help via the Elastic forum, but did not receive any answer yet.
My questions
1. Is there any source of information on this particular topic? I am aware of the fact that the plugin API changes frequently, hence I do not expect to find an extensive documentation. Maybe a good example?
Am I completely off course with the way I am trying to achieve it?
Thanks for reading this, any help would be much appreciated!

Actions on Google (Handle fallback)

I have a query on a Google home (Dialogflow).
In a specific, after I execute fallback intent three times it exits with a statement
Sorry I can't help
But it should prompt
I am ending this session see you again later.
Here is code of fallback intent
app.intent('Default Fallback Intent', (conv) =>
{
const repromptCount = parseInt(conv.arguments.get('REPROMPT_COUNT'));
if (repromptCount === 0) { conv.ask(`Hey are you listening?`); }
else if (repromptCount === 1) { conv.ask(`Are you still around?`); }
else if (conv.arguments.get('IS_FINAL_REPROMPT')) { conv.close(`I am ending this session see you again later.`); }
});
I am assuming you're trying to follow the directions in the documentation about dynamic reprompts for "no-input" type responses.
The problem appears to be that you're trying to use this for the Fallback Intent, which is not specifically triggered on a NO_INPUT event. So it is doing the test, and neither the REPROMPT_COUNT nor IS_FINAL_REPROMPT arguments are set.
If you're using the multivocal library, it will keep counters of all the Intents and Actions that are called (both for the session and sequentially) and has some macros that assist in your responses.
If you want to use the existing library, you'll need to keep track of this yourself and either store this in a Context or store it in the session data object.
If you intended to use this as part of a "no-input" response, you need to make sure you're using this with an Intent that has the actions_intent_NO_INPUT event set.

{guzzle-services} How to use middlewares with GuzzleClient client AS OPPOSED TO directly with raw GuzzleHttp\Client?

My middleware need is to:
add an extra query param to requests made by a REST API client derived from GuzzleHttp\Command\Guzzle\GuzzleClient
I cannot do this directly when invoking APIs through the client because GuzzleClient uses an API specification and it only passes on "legal" query parameters. Therefore I must install a middleware to intercept HTTP requests after the API client prepares them.
The track I am currently on:
$apiClient->getHandlerStack()-push($myMiddleware)
The problem:
I cannot figure out the RIGHT way to assemble the functional Russian doll that $myMiddleware must be. This is an insane gazilliardth-order function scenario, and the exact right way the function should be written seems to be different from the extensively documented way of doing things when working with GuzzleHttp\Client directly. No matter what I try, I end up having wrong things passed to some layer of the matryoshka, causing an argument type error, or I end up returning something wrong from a layer, causing a type error in Guzzle code.
I made a carefully weighted decision to give up trying to understand. Please just give me a boilerplate solution for GuzzleHttp\Command\Guzzle\GuzzleClient, as opposed to GuzzleHttp\Client.
The HandlerStack that is used to handle middleware in GuzzleHttp\Command\Guzzle\GuzzleClient can either transform/validate a command before it is serialized or handle the result after it comes back. If you want to modify the command after it has been turned into a request, but before it is actually sent, then you'd use the same method of Middleware as if you weren't using GuzzleClient - create and attach middleware to the GuzzleHttp\Client instance that is passed as the first argument to GuzzleClient.
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Command\Guzzle\GuzzleClient;
use GuzzleHttp\Command\Guzzle\Description;
class MyCustomMiddleware
{
public function __invoke(callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
// ... do something with request
return $handler($request, $options);
}
}
}
$handlerStack = HandlerStack::create();
$handlerStack->push(new MyCustomMiddleware);
$config['handler'] = $handlerStack;
$apiClient = new GuzzleClient(new Client($config), new Description(...));
The boilerplate solution for GuzzleClient is the same as for GuzzleHttp\Client because regardless of using Guzzle Services or not, your request-modifying middleware needs to go on GuzzleHttp\Client.
You can also use
$handler->push(Middleware::mapRequest(function(){...});
Of sorts to manipulate the request. I'm not 100% certain this is the thing you're looking for. But I assume you can add your extra parameter to the Request in there.
private function createAuthStack()
{
$stack = HandlerStack::create();
$stack->push(Middleware::mapRequest(function (RequestInterface $request) {
return $request->withHeader('Authorization', "Bearer " . $this->accessToken);
}));
return $stack;
}
More Examples here: https://hotexamples.com/examples/guzzlehttp/Middleware/mapRequest/php-middleware-maprequest-method-examples.html

Sails v1 new machine-based actions and custom responses

I'm in the middle of upgrading our API from Sails v0.12 -> v1, which was prompted by the use of self-validating machines for controller actions. After finally getting through a ton of headache replacing deprecated code, I've landed in a rough spot...
With v0.12 (rather, with the older "req, res" controller style), one could use custom response handlers across the board. I've taken advantage of this, and have request logging at the end of all our response types (with some additional sugaring of data). This was done to log all requests in the database, so we can get insights into what our production servers are doing (because they are load-balanced, having a central place to view this is a must, and this was an easy route to take).
So now, my problem is moving forward with "Actions2" machine-style actions. How does one use these custom response types in these things? Are we being forced to repeat ourselves in our exists? I can't find any good documentation to help guide this process, nor can I find a consistent way to "hook" into the end of a response using machines as actions. I can't find any documentation on what kind of options machines can give to Sails.
#Nelson yes, I understand that, but at the time, that isn't what I wanted at all. I wanted all of the benefits of Actions2.
EDIT: While the original, crossed-out comment below does still work, the prefered way to use Actions2 and the custom responses folder paradigm, is to do something similar to the following in an Actions2 file:
module.exports = {
friendlyName: 'Human-friendly name of function',
description: 'Long description of function and what it does.',
inputs: {
userCommand: {
type: 'string',
required: true,
description: 'Long, human-readable description of the input'
}
},
exits: {
success: {
responseType: 'chatbotResponse'
}
},
fn: async function(inputs, exits){
// do some crazy stuff with the inputs, which has already been validated.
return exits.success('Woot');
}
}
This ultimately will route through the responses/chatbotResponse.js, which looks something similar to this:
module.exports = async function chatbotResponse(data){
let res = this.res,
req = this.req;
if (!data) {
data = 'Something didn\'t go as planned...';
}
// how to call a Node Machine style helper with named inputs
await sails.helpers.finalizeRequestLog.with({req: req, res: res, body: {plainString: data}});
return res.json(data);
};
ORIGINAL:
As it turns out, in the Actions2 function, you just need to add the env param async function(inputs, exists, env). The env will give you access to the req and res. So, if you have custom responses, that perform special tasks (like request logging), you can just use return await env.res.customResponse('Hurray, you made a successful call!');

Query option 'Format' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings

I'm getting an exception on my Web API controller endpoint and I would appreciate some help solving it.
Here is the story:
In my Web API project, a controller exposes the following endpoints:
My Kendo UI Datagrid makes the following request:
http://localhost:63865/api/employees/GetAll?$callback=jQuery21109420544053427875_1410883352953&%24inlinecount=allpages&%24format=json&%24top=5
I'm getting this exception when validating the ODataQueryOptions sent in the request:
Query option 'Format' is not allowed. To allow it, set the 'AllowedQueryOptions' property on EnableQueryAttribute or QueryValidationSettings.
But I've enabled all query options:
[EnableQuery(AllowedQueryOptions=AllowedQueryOptions.All)]
What am I doing wrong ?
Finally manage to get to the bottom of this!
After creating the ODataValidationSettings object I needed to change the AllowedQueryOptions to AllowedQueryOptions.All. Be default all options are there except the Format and SkipToken.
Anyway, hope this may help anyone else facing the same issue.
I had the same issue but I had to solve it a different way. My OData service controll was generated using Entity Framework (data first) and it turns out Microsoft's template doesn't support all the OData parameters. This puzzled me because I don't need the extra parameters anyway because I was just trying to define a datasource for use with a TreeView Kendo component. I finally found the Telerik blog post that went into detail about Using Kendo UI With MVC4, WebAPI, OData And EF which focussed on the 'inlinecount' param but for me the culprit was the 'format' param.
I'm pasting the summary from that page here in case the link goes bad.
Remove Inline Count Parameter
transport: {
read: {
url: "/api/Albums", // <-- Get data from here
dataType: "json" // <-- The default was "jsonp"
},
parameterMap: function (options, operation) {
var paramMap = kendo.data.transports.odata.parameterMap(options);
delete paramMap.$inlinecount; // <-- remove inlinecount parameter
delete paramMap.$format; // <-- remove format parameter
return paramMap;
}
}