How do I write the Joi schema for a file that I am sending as a response?
My route returns this return h.file(filename, { mode: 'attachment'}).code(201); and well, the content-dispostion response header is attachment; filename=entries.csv.
I can maybe check the object structure of the response that's going out but is there a way Joi provides a property to check for files in the response?
Here's the Github issue you might wanna track
I misunderstood the question - it was about validating response headers, not request ones.
Short answer: it cannot be done.
Long answer:
Based on hapijs 17.5.3 https://hapijs.com/api#-routeoptionsresponseoptions it seemed doable with a function:
server.route({
method: 'GET',
path: '/file',
options: {
handler: (request, h) => {
return h.file('foobar.csv', { mode: 'attachment'}).code(201);
},
response: {
schema: async (value, options) => {
console.log('validating response:', value);
}
}
}
});
But this approach doesn't work.
It's not supported by hapijs, you'll get an exception from line 151: https://github.com/hapijs/hapi/blob/76fcd7fa97747c92501b912d64db459d7172cb26/lib/validation.js
which is:
if (!response.isBoom &&
request.response.variety !== 'plain') {
throw Boom.badImplementation('Cannot validate non-object response');
}
here's how you can validate headers on requests:
'use strict';
const Joi = require('joi');
const ErrorHandler = require('../handlers/errorHandler');
const fileUploadValidator = {
config: {
validate: {
params: {
env: Joi.string().min(2).max(10).required()
},
query: {
id: Joi.number().integer().min(0).required()
},
headers: Joi.object({
'x-request-id': Joi.string().guid().required(),
'content-disposition': Joi.string().regex(/attachment;\s*filename=.+\.csv/gi).insensitive().required()
}).options({ allowUnknown: true }),
failAction: ErrorHandler.apply_genericHandler
}
}
};
module.exports = fileUploadValidator;
Route definition:
server.route({
method: 'POST',
path: '/{env}/v1/fileUpload',
handler: FileUploadHandler.apply,
options: FileUploadValidator.config
});
you may need to tweak it a bit. I've built it based on your question.
Related
Added in a pre-requisite for the endpoint to validate that the client information being passed is legit or it will throw an error. The clientProfileValidation.clientProfileValidation method receives the request object and returns a profile object that gets attached to the request.pre.
When trying to update my route unit test, I get the below error.
Unhandled rejection occurred. One of your test may have failed silently.
TypeError: Cannot read properties of undefined (reading 'routes')
This is a nodejs api using HAPI framework. When I remove the pre from the route, the test passes. I attempted to mock the clientProfileValidation method but its not working as expected.
Route
const drayageRampRecommendation = {
method: 'POST',
path: '/endpoint',
handler: async (request, h) => {
try {
const resp = await rampHandler.rampRecommendation(request);
return h.response(resp).code(201);
} catch (error) {
return handleError(error).toBoom();
}
},
config: {
pre: [
{
method: clientProfileValidation.clientProfileValidation,
assign: 'profile'
}
],
payload: {
allow: ['application/json', 'application/*+json']
}
}
};
Unit Test:
Using the Tape and Test Double Libraries for testing
test('drayage/recommend-ramps route: should return 201 when successfully processed', async (t) => {
beforeEachRampRecommendation();
const options = {
method: 'POST',
url: '/endpoint',
payload: recommendRampFixture,
headers: { authorization: 'Bearer 123' },
auth: {
credentials: { user: 'test', clientId: 'testClient' },
strategy: 'default'
}
};
const testProfile = {
_id: 'testId',
auth0ClientName: 'test client'
};
td.when(clientProfileValidation.clientProfileValidation(), {
ignoreExtraArgs: true
}).thenReturn(testProfile);
td.when(recommendRampHandler.rampRecommendation(), {
ignoreExtraArgs: true
}).thenReturn('');
const server = await buildServer(routes);
const response = await server.inject(options);
t.equal(response.statusCode, 201, 'Should return 201 status code');
td.reset();
t.end();
});
Here is the code for my server which works fine. I am trying to achieve this with netlify's serverless functions which I have pasted further below.
CODE ON STANDARD SERVER-HEROKU
const ratingController = {};
const Rating = require("../models/ratingModel");
ratingController.getAllRatings = async function (req, res) {
const rating = await Rating.find();
res.status(200).json({
status: "success",
data: rating,
});
};
ratingController.createOneRating = async function (req, res) {
console.log(req.body);
req.body.userIp = req.headers["x-forwarded-for"];
const rating = await Rating.create(req.body);
// const rating = new Rating(req.body);
// await rating.save();
res.status(200).json({
status: "success",
data: {
rating,
},
});
};
PART 1 - GET REQUEST
Here's my code for the getAllRatings and it works fine
SERVERLESS FUNCTION - NETLIFY
const { MongoClient } = require("mongodb");
require("dotenv").config();
exports.handler = async function getData(event, context) {
const client = await MongoClient.connect(process.env.DB, {
useUnifiedTopology: true,
useNewUrlParser: true,
});
const db = client.db();
try {
const slug = event.queryStringParameters.id;
const data = await db.collection("collectionName").find({ slug }).toArray();
client.close();
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: "success",
data: data,
}),
};
} catch (error) {
console.log(error);
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: "fail",
message: error.message,
}),
};
}
};
My first question for the above is
Just because it works may not mean it's the right way to do it.. I had a few concerns if calling the database each time there's a call is correct and then placing the code the way I have, if it's the way it should be. It's all based on testing and random research. There's no real method being followed so would appreciate some guidance on a more efficient method to do this.
Normally on a regular server the database connection is done just once.. and here I seem to be doing it every time and I am a bit confused if that's ok or not..
PART 2 - POST REQUEST
Here's my code for the POST request createOneRating
SERVERLESS FUNCTION - NETLIFY
const { MongoClient } = require("mongodb");
require("dotenv").config();
exports.handler = async function createRating(event, context) {
const client = await MongoClient.connect(process.env.DB, {
useUnifiedTopology: true,
useNewUrlParser: true,
});
const db = client.db();
try {
console.log(event);
const rating = await db.collection("ratings").insertOne(event.body);
client.close();
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: "success",
data: rating,
}),
};
} catch (error) {
console.log(error);
return {
statusCode: 400,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: "fail",
message: error.message,
}),
};
}
};
This one does not work as it says
{
"status": "fail",
"message": "Cannot create property '_id' on string ''"
}
And I guess that's because event.body is not where the data is.. but I am not sure how to get the POST data in a serverless deployment.
So my second question is
How do I retrieve the data sent by a POST request. As there's no request parameter I am a bit confused.
Also I'd like to add the IP of the user so other than the POST data I'd also need some help on how to do this
req.body.userIp = req.headers["x-forwarded-for"];
Based on my own research, I have answers to the questions and am placing them here for my own reference and for those who might face a similar situation in the future.
Question 1 : Is it ok to make a database connection on every call that's made to a serverless function
It seems it's ok to do this and for those, like me, who thought that maybe it was not the right way to do it, it's definitely not the wrong way. Maybe there's an efficient way to do this and I'd be open to learn more about this if possible. But for now, it's good to know that it's not wrong to connect to the database each time you make a call.
Question 2: How to make a POST request on a serverless as there's no request parameter
I was not aware that the event parameter is in fact a replacement for the request parameter and that the headers and body are properties of the event object and can be accessed in the same way ie event.body and event.headers. Here's a link that could save you some time to confirm this.
(https://docs.netlify.com/functions/build-with-javascript/#synchronous-function-format)
And if you, like me, don't know if a serverless function can be defined as GET or POST or run into an issue where the POST request gets converted into a GET when making a function call here's a link that would help.
How to define Netlify function endpoint as POST?
I am currently struggling with Nuxt's Axios module: https://axios.nuxtjs.org/.
I would like to get some data from a specific endpoint where I have to use Basic Authentication.
Normally, with Axios, I would do something like:
await axios.get(
'http://endpoint',
{},
{
withCredentials: true,
auth: {
username: 'userame',
password: 'pw'
}
}
)
Unfortunately, with Nuxt's Axios module, it seems it is not that easy...
I tried something like:
const data = await this.$axios.$get(
'http://endpoint',
{},
{
credentials: true,
auth: {
username: 'user',
password: 'pw'
}
}
)
But that leaves me with a 401 Unauthorized...
What am I missing here?
The second argument to axios.get() (and $axios.$get()) is the Axios config, but you've passed it as the third argument (which is effectively ignored). The API likely omits the data argument from axios.get() because data doesn't apply in this context.
The solution is to replace the 2nd argument with your config:
const data = await this.$axios.$get(
'http://endpoint',
// {}, // <-- Remove this! 2nd argument is for config
{
credentials: true,
auth: {
username: 'user',
password: 'pw'
}
}
)
So I wrote Slack reporter for my automated tests and wanted to switch from deprecated module 'request' to 'https' module. I changed the request sending a normal message but I don't know how to create a request for sending a file. I can't find any example in node documentation (no POST examples for 'https' there) nor any example of that kind of use on the internet. Can anyone help me with this?
That's the working request:
function sendLogFile() {
console.log("Sending log file to slack...");
return new Promise((resolve, reject) => {
request.post(
{
url: fileUploadUrl,
formData: {
token: token,
method: "POST",
title: "Test Log File",
filename: "testLog.txt",
filetype: "auto",
channels: "***",
file: fs.createReadStream("testLog.txt")
}
},
function(err, response) {
if (response.body.includes("created"))
resolve("File send successfully!");
if (response.body.includes("error")) reject(response.body);
if (err) reject(err);
}
);
});
}
And this is kinda (SEE THE EDIT BELOW) what I want (but it's not working):
function sendLogFile() {
return new Promise((resolve, reject) => {
const requestOptions = {
url: fileUploadUrl,
headers: headers,
formData: {
token: token,
method: "POST",
title: "Test Log File",
filename: "testLog.txt",
filetype: "auto",
channels: "***",
file: fs.createReadStream("testLog.txt")
}
};
const req = https.request(requestOptions, res => {
res.on("data", d => resolve(d));
});
req.on("error", e => {
reject(e);
});
// Probably that's the part where I'm stuck:
req.write('????????')
req.end();
});
}
I know there is slackapi for node but the thing is I need this reporter to be without any additional packages. And I know it's possible with request.promise or xhr but I need this to be 'https'.
EDIT:
Ok, so I was trying to get somewhere and I think it should look more like:
const file = fs.createReadStream("testLog.txt");
const options = {
channels: "***",
hostname: "slack.com",
port: 443,
path: '/api/files.upload',
method: 'POST',
headers: {
'Authorization': "Bearer ***",
'Content-Type': 'application/json; charset=utf-8',
}
}
const req = https.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
res.on('data', d => {
process.stdout.write(d)
})
})
req.on('error', error => {
console.error(error)
})
req.write(data)
req.end()
But I have no idea how to past file to req.write(data) since 'data' has to be string, Buffer, ArrayBuffer, Array, or Array-like Object.
So, for now, the response I get is:
statusCode: 200 {"ok":false,"error":"no_file_data"}%
And I'm also not sure if it's possible because slack API says the header should be formData but this response suggests this approach is fine I guess.
Anyone, please?
If you refer to https documentation you can see that options object does not accept such property as formData.
Instead, you should try to send the post data like in this answer.
I am doing a request to my server to patch some regular form fields as well as upload an image.
If I do these alone in separate requests all works perfectly, but nothing works if the form data and the file are in the same request. This is what it currently looks like:
const file = new FormData()
file.append('uploadedFile', uploadedFile, uploadedFile.name)
const patch = {
name: 'Some Name',
age: 18,
file
}
return isomorphicFetch(`${MY_URL}`, {
headers: {
Accept: 'application/json'
}
method: 'PATCH',
body: patch
})
Here is what my middleware looks like:
const multipartMiddleware = multer({ dest: 'uploads/' })
app.use('/myendpoint',
multipartMiddleware.single('uploadedFile'),
(req, res, next) => {
console.log('REQ', req)
req.feathers.file = req.file
next()
}
)
Here is the thing: if I only put file as the body of my PATCH, it works just fine, I will have a req.file object in the middleware as expected, however, with the patch object I show in my example, I don't have a req.file object. What am I doing wrong? I would like to keep both my files and the name and age fields.
Keep in mind that if I split it into two calls, it works just as expected. For it to work, it currently looks like this:
// This code is the only way I can make my patches work:
const file = new FormData()
file.append('uploadedFile', uploadedFile, uploadedFile.name)
const patch = {
name: 'Some Name',
age: 18,
}
return Promise.all([
isomorphicFetch(`${MY_URL}`, {
headers: {
Accept: 'application/json'
},
method: 'PATCH',
body: patch
}),
isomorphicFetch(`${MY_URL}`, {
headers: {
Accept: 'application/json'
},
method: 'PATCH',
body: file
})
])
Update: I tried doing this through Postman, and it did in fact work. So I am even more confused now.