Canva publish extension API : Endpoint never get call - node.js

I try to make a Canva App with a publish extension.
I just follow the Quick start (https://docs.developer.canva.com/apps/extensions/publish-extensions/quick-start) with Glitch and it work well on it.
But when I try to put in on my own public host name, with a other port (like http://mydomaine.com:3000) Canva NEVER call my endpoint. I just write a log file of every action on my app post and I never get a update on it, and when I try the app on Canva.com it just show me a error message.
//Copy from the Quick Start
app.post('/publish/resources/upload', async (request, response) => {
try{
writeLog("Uploading file");
await fs.ensureDir(path.join(__dirname, 'export'));
// Get the first asset from the "assets" array
const [asset] = request.body.assets;
// Download the asset
const image = await jimp.read(asset.url);
const filePath = path.join(__dirname, 'export', asset.name);
await image.writeAsync(filePath);
// Respond with the URL of the published design
response.send({
type: 'SUCCESS',
url: url.format({
protocol: request.protocol,
host: request.get('host'),
pathname: asset.name,
}),
});
} catch (err) {
writeLog("ERROR (app.post('/publish/resources/upload'): " + err);
}
});
//Just log on the log file
function writeLog(log){
// fs.appendFile(path.join(__dirname, '/log/' + `${month}/${date}/${year}` +'log.txt'), dateDisplay + "|" + log + "\n", (err) => {
// if (err) throw err;
// });
var today = new Date();
var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
var date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate();
var dateTime = date + ' ' + time;
natifFS.appendFile('log.txt', dateTime + '| '+ log + "\n", (err) => {
if (err) throw err;
});
}
Last thing, when I try to call a post request on the same endpoint as Canva (/publish/resources/upload) with Postman, I get a update on my log.txt file
If anyone has idea, thank you.

Related

Lambda function taking >3 seconds to run + 5-10 secs warmup each time

I have a simple node.js function with 2 REST API calls and a socket connection output hosted in an AWS lambda. It takes 5-10 secs warmup time and >3+ secs execution time.
When the code is run locally it executes both requests, socket connection and completes in about ~1300ms. Why is AWS more then double the execution time? I have set-timeout to 120seconds and memory at 128mb (default).
I appreciate the code is not very tidy; I am working on cleaning it but needed something going for the time being.
The project simply gets info from ServiceM8 via API when called by a webhook subscription, then formats the info into ZPL strings and forwards them to a tcp server for printing via thermal printer.
My questions are:
Is it my code bottle necking?
Can it be optimized to run faster?
Do i simply need to employ a warming plugin for my function to allow hot starting?
My function:
'use strict';
//Require libraries
var request = require("request");
var net = require('net');
exports.handler = (event, context, callback) => {
if (event.eventName != 'webhook_subscription') {
callback(null, {});
}
//Global Variables
var strAssetUUID;
var strAssetURL;
var strFormUUID;
var strTestDate;
var strRetestDate;
var appliancePass = true;
var strAccessToken;
var strResponseUUID;
//Printer Access
const tcpUrl = 'example.com';
const tcpPort = 12345;
var client = new net.Socket();
//UUID of Appliance Test Form.
const strTestFormUUID = 'UUID_of_form';
//Begin function
/**
* Inspect the `eventArgs.entry` argument to get details of the change that caused the webhook
* to fire.
*/
strResponseUUID = event.eventArgs.entry[0].uuid;
strAccessToken = event.auth.accessToken;
console.log('Response UUID: ' + strResponseUUID);
console.log('Access Token: ' + strAccessToken);
//URL Options for FormResponse UUID query
const urlFormResponse = {
url: 'https://api.servicem8.com/api_1.0/formresponse.json?%24filter=uuid%20eq%20' + strResponseUUID,
headers: {
// Use the temporary Access Token that was issued for this event
'Authorization': 'Bearer ' + strAccessToken
}
};
//Query form Response UUID to get information required.
request.get(urlFormResponse, function(err, res, body) {
//Check response code from API query
if (res.statusCode != 200) {
// Unable to query form response records
callback(null, {err: "Unable to query form response records, received HTTP " + res.statusCode + "\n\n" + body});
return;
}
//If we do recieve a 200 status code, begin
var arrRecords = JSON.parse(body);
//Store the UUID of the form used for the form response.
strFormUUID = arrRecords[0].form_uuid;
console.log('Form UUID: ' + strFormUUID);
//Store the UUID of the asset the form response relates to.
strAssetUUID = arrRecords[0].asset_uuid;
console.log('Asset UUID: ' + strAssetUUID);
if (strFormUUID == strTestFormUUID){
//Get the edited date and parse it into a JSON date object.
var strEditDate = new Date(arrRecords[0].edit_date);
//Reassemble JSON date to dd-mm-yyyy.
strTestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
//Extract the response for retest period.
var strRetestAnswer = JSON.parse(arrRecords[0].field_data);
strRetestAnswer = strRetestAnswer[0].Response;
//Appropriate function based on retest response.
switch(strRetestAnswer) {
case '3 Months':
//Add x months to current test date object
strEditDate.setMonth(strEditDate.getMonth() + 3);
strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
break;
case '6 Months':
strEditDate.setMonth(strEditDate.getMonth() + 6);
strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
break;
case '12 Months':
strEditDate.setMonth(strEditDate.getMonth() + 12);
strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
break;
case '2 Years':
strEditDate.setMonth(strEditDate.getMonth() + 24);
strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
break;
case '5 Years':
strEditDate.setMonth(strEditDate.getMonth() + 60);
strRetestDate = strEditDate.getDate() + '/' + (strEditDate.getMonth() + 1) + '/' + strEditDate.getFullYear();
break;
default:
strRetestDate = "FAIL";
appliancePass = false;
}
console.log('Appliance Pass: ' + appliancePass);
console.log('Test Date: ' + strTestDate);
console.log('Retest Period: ' + strRetestAnswer);
console.log('Retest Date: ' + strRetestDate);
//URL Options for Asset UUID query
const urlAssetResponse = {
url: 'https://api.servicem8.com/api_1.0/asset/' + strAssetUUID + '.json',
headers: {
// Use the temporary Access Token that was issued for this event
'Authorization': 'Bearer ' + strAccessToken
}
};
//Query the api for the asset URL of the provided asset UUID.
request.get(urlAssetResponse, function(err, res, body) {
//Check response code from API query
if (res.statusCode != 200) {
// Unable to query asset records
callback(null, {err: "Unable to query asset records, received HTTP " + res.statusCode + "\n\n" + body});
return;
}
//If we do recieve a 200 status code, begin
var strAssetResponse = JSON.parse(body);
//Store the asset URL
strAssetURL = 'https://sm8.io/' + strAssetResponse.asset_code;
console.log('Asset URL: ' + strAssetURL);
//generate tag and send to printer
var strZPLPass = ('^XA....^XZ\n');
var strZPLFail = ('^XA....^XZ\n');
//Now that we have our ZPL generated from our dates and URLs
//Send the correct ZPL to the printer.
client.connect(tcpPort, tcpUrl, function() {
console.log('Connected');
//Send Appropriate ZPL
if (appliancePass) {
client.write(strZPLPass);
}else {
client.write(strZPLFail);
}
console.log('Tag Successfully Printed!');
//As the tcp server receiving the string does not return any communication
//there is no way to know when the data has been succesfully received in full.
//So we simply timeout the connection after 750ms which is generally long enough
//to ensure complete transmission.
setTimeout(function () {
console.log('Timeout, connection closing...');
client.destroy();
}, 750);
});
});
}
});
};
First of all, I would suggest you stop using the request module and switch to native. Everything can be done without a tons of lines these days. request is a module with 48 total dependencies; if you do the math, that's thousands of lines for a simple GET request.
You should always minimize the complexity of your dependencies. I use a Lambda to check the health of my sites, grabbing the whole request and checking the HTML on completely different servers. VPS is located in Frankfurt, AWS in Ireland. My ms/request is ranging between 100~150 ms.
Here's a simple promise request I'm using:
function request(obj, timeout) {
return new Promise(function(res, rej) {
if (typeof obj !== "object") {
rej("Argument must be a valid http request options object")
}
obj.timeout = timeout;
obj.rejectUnauthorized = false;
let request = http.get(obj, (response) => {
if (response.statusCode !== 200) {
rej("Connection error");
}
var body = '';
response.on('data', (chunk) => {
body += chunk;
});
response.on('end', () => {
res(body);
});
response.on('error', (error) => {
rej(error);
});
});
request.setTimeout(timeout);
request.on('error', (error) => {
rej(error);
})
request.on('timeout', () => {
request.abort();
rej("Timeout!")
})
});
}
Example
const reqOpts = {
hostname: 'www.example.com',
port: 443,
path: '/hello',
method: 'GET',
headers: {
handshake: "eXTNxFMxQL4pRrj6JfzQycn3obHL",
remoteIpAddress: event.sourceIp || "lambda"
}
}
try {
httpTestCall = await request(reqOpts, 250);
}
catch (e) {
console.error(e);
}
Now based on that change switch your handler to async using exports.handler = async(event, context, callback) => {} and use console to measure the execution time of every request using console.time() and console.timeEnd() for your request or anything. From there you could see what's stepping down your code using Cloudwatch logs. Here's another example based on your code:
let reqOpts = {
hostname: 'api.servicem8.com',
port: 443,
path: '/api_1.0/formresponse.json?%24filter=uuid%20eq%20' + strResponseUUID,
method: 'GET',
headers: {
// Use the temporary Access Token that was issued for this event
'Authorization': 'Bearer ' + strAccessToken
}
}
console.time("=========MEASURE_servicem8=========")
let error = null;
await request(reqOpts, 5555).catch((e)=>{
error = e;
})
console.timerEnd("=========MEASURE_servicem8=========")
if (error){
callback(null, {err: "Unable to query form response records, received HTTP" + error}); /* or anything similar */
}
References
https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html
https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html
aws lambdas are not fast by nature (as of writing this answer). The startup time is not guaranteed, and it is known to be high.
If you need performance - you will not get it this way.

express res.download serving up 0 byte files

res.download is serving up the file but once downloaded it is 0 bytes?
Any ideas?
app.get('/download', function(req, res) {
console.log("download");
console.log(req.query.fileID);
fileDownload(req.query.fileID, function(rep){
if(rep.success){
console.log("Serving File to User, File: " + rep.data);
res.download(__dirname + "/" + rep.data, rep.data)
}else{
console.log(res);
}
});
})
A ls on the folder shows the file is there ready for download, the names are correct and all on download box that browser displays but download is always 0 bytes in size.
A check on the file from file file download shoes yes it is there and yes its all good.
ISSUE FOUND MAYBE:
I Think the issue is the file is not fully downloaded before its being served to client, see below,i will try adding a callback to the PIPE.....
function fileDownload(id, callback){
info(id, function(res){
if(!res.error){
info(id, function(res){
if(!res.error){
//console.log(res.data);
var d = JSON.parse(res.data);
//console.log(d['file_name']);
var url2 = baseurl + "/api/file/" + id ;
var r = request(url2);
r.on('response', function (res) {
res.pipe(fs.createWriteStream('./' + d['id'] + d['file_name']));
console.log("Download Done: " + './' + d['id'] + d['file_name']);
return callback({success:true, data:d['id'] + d['file_name']});
});
}else{
console.log("ERROR: " + res.data)
return callback({success:false, data: res.data});
}
});
}else{
console.log("ERROR: " + res.data)
return callback({success:false, data: res.data});
}
});
};
The issue was due to the file being served up before the stream was finished writing,
fix add .on('close') to the stream.
r.on('response', function (res) {
res.pipe(fs.createWriteStream('./' + d['id'] + d['file_name']).on('close', function() {
console.log('file done');
console.log("Download Done: " + './' + d['id'] + d['file_name']);
return callback({success:true, data:d['id'] + d['file_name']});
}));
console.log("You should not see this");
});

how to extract data from spotify npm package? I keep getting undefine

===================This is my code below=========================
function spotifyThisSong() {
var spotify = new Spotify({
id: 'myid',
secret: 'mysecret'
});
var songName = process.argv[3];
if(!songName){
songName = "What's my age again";
}
var params = songName;
spotify.search({ type: 'track', query: params }, function(err, data) {
if ( err ) {
console.log('Error occurred: ' + err);
return; //from spotify npm docs
}
else{
console.log(data);
};
});
}
===================END OF CODE=========================
keeps giving me undefine. I need to extract the song name, year, album and its url. Thanks in advance.
//So this is what I found. This actually involves a few things.
//FIRST, capture user-input,
//SECOND, npm install spotify, and use the template as suggested in the DOCS.
//THIRD, parse through the JSON correctly as done below.
//HOPE this helps someone. (dont forget your keys from spotify)
var songName = process.argv[3]; //capture userInput and query it below
params = songName;
spotify.search({ type: 'track', query: params }, function(err, data) {
if ( err ) {
console.log('Error occurred: ' + err);
return;
}
else{
output = space + "================= DATA HERE ==================" +
space + "Song Name: " + "'" +songName.toUpperCase()+ "'" +
space + "Album Name: " + data.tracks.items[0].album.name +
space + "Artist Name: " + data.tracks.items[0].album.artists[0].name +
space + "URL: " + data.tracks.items[0].album.external_urls.spotify + "\n\n\n";
console.log(output);
};
});

Angular download file from byte array sent from node.js

I think I'm very close to what I want to do. I have the following api get method in node.js that is retrieving a file varbinary(MAX) from an SQL Server database. It was converted from a base64 encoded string before inserted so the Content Type information was stripped from the string.
node.js
router.get('/getFile', (req, res) => {
console.log("Calling getFile for file " + req.query.serialNumber + ".")
var serialNumber = req.query.serialNumber;
let request = new sql.Request(conn);
request.query('SELECT FileName + \'.\' + FileExtension AS \'File\', FileType, ContentType, SerialNumber, Chart ' +
'FROM dbo.ChangeFiles ' +
'WHERE SerialNumber = ' + serialNumber)
.then(function (recordset) {
log("Successfully retrieved file " + recordset[0].SerialNumber + " from database.");
log("Length of blob " + recordset[0].File + " is " + recordset[0].Chart.length)
res.status(200);
res.setHeader('Content-Type', recordset[0].ContentType);
res.setHeader('Content-Disposition', 'attachment;filename=' + recordset[0].File);
res.end(Buffer.from((recordset[0].Chart)));
}).catch(function (err) {
log(err);
res.status(500).send("Issue querying database!");
});
});
That works fine, but what to do in Angular to retrieve it and prompt for a download for the user has not been clear for me, nor has there been a lot as far as help/resources online. Here is what I have so far in my service class.
fileDownload.service.ts
downloadFile(serialNumber: string): Observable<any> {
return this.http.get(this.baseURL + '/getFile', { params: { serialNumber: serialNumber } })
.map(this.extractFile);
}
private extractFile(response: Response) {
const file = new Blob([response.blob]);
FileSaver.saveAs(file);
// const url = window.URL.createObjectURL(file);
// window.open(url);
return file;
}
As you can see I've tried a couple of approaches. The commented out portion of the extractFile method didn't work at all, and using the FileSaver.saveAs function produces a file download of an unknown type, so the headers sent from node.js didn't seem to affect the file itself.
Would someone be able to advise how to proceed in Angular with what is successfully being sent from node.js so that I can successfully download the file, regardless of type?
Thanks so much in advance.
I got it working afterall. I had to rework the api call so that it sent all of the file information separately so that the MIME type, and file name can be assigned to the file on the client side in the component class. For some reason when I tried to do so all in the api, it wouldn't work so that was my work around. So here is what works for me.
node.js api
router.get('/getFile', (req, res) => {
console.log("Calling getFile for file " + req.query.serialNumber + ".")
var serialNumber = req.query.serialNumber;
let request = new sql.Request(conn);
request.query('SELECT FileName + \'.\' + FileExtension AS \'File\', FileType, ContentType, SerialNumber, Chart ' +
'FROM dbo.ChangeFiles ' +
'WHERE SerialNumber = ' + serialNumber)
.then(function (recordset) {
log("Successfully retrieved file " + recordset[0].SerialNumber + " from database.");
log("Length of blob " + recordset[0].File + " is " + recordset[0].Chart.length)
res.send(recordset[0]);
}).catch(function (err) {
log(err);
res.status(500).send("Issue querying database!");
});
});
component class
downloadFile(serialNumber: string): void {
this.changeService.downloadFile(serialNumber).subscribe((res: any) => {
const ab = new ArrayBuffer(res.Chart.data.length);
const view = new Uint8Array(ab);
for (let i = 0; i < res.Chart.data.length; i++) {
view[i] = res.Chart.data[i];
}
const file = new Blob([ab], { type: res.ContentType });
FileSaver.saveAs(file, res.File);
console.log(res);
});
}
service class
downloadFile(serialNumber: string): Observable<any> {
return this.http.get(this.baseURL + '/getFile', { params: { serialNumber: serialNumber } })
.map(this.extractFile);
}
private extractFile(response: Response) {
// const file = new Blob([response.blob]);
// FileSaver.saveAs(file);
// const url = window.URL.createObjectURL(file);
// window.open(url);
const body = response.json();
return body || {};
}
Update your code to call subscribe instead of map

Change the file name on the fly for downloading

I am saving user uploads by renaming it's original name to userID + '_' + new Date().getTime() + fileExt. I am storing the file properties in mongodb collections as :
{
name : String //generated name
, originalName : String //its original name
...
}
Now when the user requests for downloading of file I have to provide the file with its original name (which is persisted in db so no problem to grab it) to the user.
For the following request
GET /users/:userId/uploads/:fileId?type=download
I have this handler
//the mongoose query
UserUpload.findById(req.params.fileId).exec(function(err, doc){
var fileLocation = __dirname + '/public/uploads/users/' + req.params.userId + '/' + doc.name;
if(req.query.type && req.query.type == 'download') {
// I don't know what should I do now
// Downloader.download(fileLocation, newName);
}
});
I read wiki of node-static module but could not figure out how to do that job?
I found the answer here : Download a file from NodeJS Server using Express . Both using express and without using express.
It is too simple if you are using Express. Here is the documentation for res.download. I can't believe that the solution is just one line of code :
res.download('/path/to/file.ext', 'newname.ext');
Here what I use in one of my project, smil is a type of file I need to download, nevermind it.
In that project I have DOWNLOAD_DIR as global variable, which contain the full path to the download folder.
It may make a lot of people cringe (especcially the fileExistSync) but it s a start.
var DOWNLOAD_DIR = '/path/to/download',
url = require('url'),
http = require('http'),
/*
download
IN_:
file_url, url of the file to download
OUT:
file_name, full path to downloaded file, null if the file cound t be downloaded.
COM:
download a file, the file name will be the last part of the url (http://why.so/serious.txt => serious.txt).
WARNING: Do NOT follow redirections.
*/
function download(file_url, callback) {
var options = {host: url.parse(file_url).host, port: 80, path: url.parse(file_url).pathname},
file_name = url.parse(file_url).pathname.split('/').pop(),
//Creating the file
file = fs.createWriteStream(DOWNLOAD_DIR + file_name, {flags: 'w', encoding: 'binary'});
console.log('Downloading file from ' + file_url);
console.log('\tto ' + file_name);
http.get(options, function (res) {
res.pipe(file, {end: 'false'});
//When the file is complete
res.on('end', function () {
//Closing the file
file.end();
console.log('\t\tDownloaded '+ file_name);
callback(DOWNLOAD_DIR + file_name);
});
});
process.on('uncaughtException', function(err) {
console.log('Can t download ' + file_url + '\t(' + err + ')', false);
callback(null);
});
}
/*
download_smil
IN_:
file_url, url of the file to download
OUT:
file_name, full path to downloaded file
COM:
Follow http redirection and then call download function to download it.
You can modify the cad function to use custom names.
*/
function download_smil(file_url, callback) {
function cad(link, callback) {
//Does the file already exist?
var file = url.parse(link).pathname.substr(url.parse(link).pathname.lastIndexOf('/') + 1),
pkmn;
pkmn = fs.existsSync(DOWNLOAD_DIR + '/' + file);
if (pkmn) {
//YES: callback
console.log('File ' + file + ' already exist, skipping');
callback(DOWNLOAD_DIR + file, true);
} else {
//NO: Download it
console.log('Will download ' + link);
download(link, callback);
}
}
//GET the page
http.get(file_url, function (res) {
var link;
//console.log('res.statusCode = ' + res.statusCode + ' res.headers.location = ' + res.headers.location);
//Check if it is a redirect
if (res.statusCode > 300 && res.statusCode < 400 && res.headers.location) {
//console.log('redirect');
//Check if the hostname is in the location
if (url.parse(res.headers.location).hostname) {
//console.log('The link to the smil is res.headers.location');
link = res.headers.location;
cad(link, callback);
} else {
//console.log('The link to the smil is url_parse(file_url).hostname + res.headers.location = ' + url_parse(file_url).hostname + res.headers.location);
link = url_parse(file_url).hostname + res.headers.location;
cad(link, callback);
}
} else {
//console.log('The url is good : ' + file_url);
cad(file_url, callback);
}
});
}

Resources