How to add media upload for BigQuery Rest API using UrlFetchApp? - google-apps-script

I need to stream data into BigQuery from my Google Apps Script addon.
But I need to use my service account only (I need to insert data into my BigQuery table, not user's BigQuery table)
I followed this example: https://developers.google.com/apps-script/advanced/bigquery#load_csv_data
Because Apps Script Advanced Service doesn't support service account natively, so I need to change this example a bit:
Instead of using Advanced Service BigQuery, I need to get the OAuth token from my service account, then using BigQuery Rest API to handle the same job:
This is what I did:
function getBigQueryService() {
return (
OAuth2.createService('BigQuery')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(PRIVATE_KEY)
.setIssuer(CLIENT_EMAIL)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Caching
.setCache(CacheService.getUserCache())
// Locking
.setLock(LockService.getUserLock())
// Set the scopes.
.setScope('https://www.googleapis.com/auth/bigquery')
)
}
export const insertLog = (userId, type) => {
const bigQueryService = getBigQueryService()
if (!bigQueryService.hasAccess()) {
console.error(bigQueryService.getLastError())
return
}
const projectId = bigqueryCredentials.project_id
const datasetId = 'usage'
const tableId = 'logs'
const row = {
timestamp: new Date().toISOString(),
userId,
type,
}
const data = Utilities.newBlob(convertToNDJson(row), 'application/octet-stream')
// Create the data upload job.
const job = {
configuration: {
load: {
destinationTable: {
projectId,
datasetId,
tableId,
},
sourceFormat: 'NEWLINE_DELIMITED_JSON',
},
},
}
const url = `https://bigquery.googleapis.com/upload/bigquery/v2/projects/${projectId}/jobs`
const headers = {
Authorization: `Bearer ${bigQueryService.getAccessToken()}`,
'Content-Type': 'application/json',
}
const options = {
method: 'post',
headers,
payload: JSON.stringify(job),
}
try {
const response = UrlFetchApp.fetch(url, options)
const result = JSON.parse(response.getContentText())
console.log(JSON.stringify(result, null, 2))
} catch (err) {
console.error(err)
}
}
As you can see in my code, I get the Blob data (which is the actual json data that I need to put in BigQuery table) using this line:
const data = Utilities.newBlob(convertToNDJson(row), 'application/octet-stream')
But I don't know where to use this data with the BigQuery Rest API
The documentation doesn't mention it: https://cloud.google.com/bigquery/docs/reference/rest/v2/jobs/insert
How this can be done? Thank you.

I can solve this problem using Tanaike's FetchApp library:
https://github.com/tanaikech/FetchApp#fetch
Anyone has this issue in the future: please check my comment in code to understand what was done.
Turn out, the job variable is treated as metadata, and the data variable is treated as file in the form data object
// First you need to convert the JSON to Newline Delimited JSON,
// then turn the whole thing to Blob using Utilities.newBlob
const data = Utilities.newBlob(convertToNDJson(row), 'application/octet-stream')
// Create the data upload job.
const job = {
configuration: {
load: {
destinationTable: {
projectId,
datasetId,
tableId,
},
sourceFormat: 'NEWLINE_DELIMITED_JSON',
},
},
}
const url = `https://bigquery.googleapis.com/upload/bigquery/v2/projects/${projectId}/jobs?uploadType=multipart`
const headers = {
Authorization: `Bearer ${bigQueryService.getAccessToken()}`,
}
const form = FetchApp.createFormData() // Create form data
form.append('metadata', Utilities.newBlob(JSON.stringify(job), 'application/json'))
form.append('file', data)
const options = {
method: 'post',
headers,
muteHttpExceptions: true,
body: form,
}
try {
FetchApp.fetch(url, options)
} catch (err) {
console.error(err)
}
Note: When you create the service account, choose role BigQuery Admin, or any role that has permission bigquery.jobs.create
https://cloud.google.com/bigquery/docs/access-control#bigquery-roles
Because if you don't, you will have the error
User does not have bigquery.jobs.create permission...

Related

Google Apps Script - URL Fetch App returning random numbers

I am new to Apps Script and was trying to build an API and call that API through a different script. I created the web app and published it.
This is the URL:
https://script.google.com/macros/s/AKfycbxKVmGy3fxDfoHxyDtQh7psqj7IdKF7qHbgxLAwNRoiKTA-bpKN4QKtArzwsYdFb-Hb/exec
When I open this link, I can see the data correctly but when I try to fetch this data from a different script using urlfetchapp, it returns random numbers. I need help on what I am doing incorrectly.
Script which I am using to call this data:
function GetCopies()
{
var options = {
'contentType': "application/json",
'method' : 'get',
};
var Data = UrlFetchApp.fetch('https://script.google.com/macros/s/AKfycbxKVmGy3fxDfoHxyDtQh7psqj7IdKF7qHbgxLAwNRoiKTA-bpKN4QKtArzwsYdFb-Hb/exec',options)
Logger.log(Data.getContent())
}
This is the log I get:
I tried parsing it, but it throws an error:
How can I get data from URL correctly?
A working sample:
Create two Google Apps Script projects. In my case API and fetcher
API
const doGet = () => {
const myObj = {
"name": "Mr.GAS",
"email": "mrgas#blabla.com"
}
return ContentService
.createTextOutput(JSON.stringify(myObj))
.setMimeType(
ContentService.MimeType.JSON
)
}
fetcher
const LINK = "API_LINK"
const fetchTheAPI = async () => {
const options = {
'contentType': "application/json",
'method': 'get',
}
const res = UrlFetchApp.fetch(LINK, options)
const text = res.getContentText()
console.log(JSON.parse(text))
}
Deploy the API: Select type > Web app and Who has access > Anyone, copy the URL (it is important to copy that URL not the one redirected in the browser)
Replace the "API_LINK" by the URL.
Run the function.
You only need to adapt this example to suit your needs.
Documentation:
Content Service
Web Apps

Resize all existing images stored in firebase storage and update the newly resized image url to database via api call [duplicate]

This question already has an answer here:
How can I resize all existing images in firebase storage?
(1 answer)
Closed 9 months ago.
I have requirement to resize new and existing images stored in firebase store. For new image, I enabled firebase's resize image extension. For existing image, how can I resized the image and get the newly resized image url to update back to database via api.
Here is my firebase function to get existing image urls from database. My question is how to resize the image and get the new image url?
const functions = require("firebase-functions");
const axios =require("axios");
async function getAlbums() {
const endpoint = "https://api.mydomain.com/graphql";
const headers = {
"content-type": "application/json",
};
const graphqlQuery = {
"query": `query Albums {
albums {
id
album_cover
}
}`
};
functions.logger.info("Call API");
const response = await axios({
url: endpoint,
method: 'post',
headers: headers,
data: graphqlQuery
});
if(response.errors) {
functions.logger.info("API ERROR : ", response.errors) // errors if any
} else {
return response.data.data.albums;
}
}
exports.manualGenerateResizedImage = functions.https.onRequest(async () => {
const albums = await getAlbums();
functions.logger.info("No. of Album : ", albums.length);
});
I think the below answer from Renaud Tarnec will definitely help you.
If you look at the code of the "Resize Images" extension, you will see that the Cloud Function that underlies the extension is triggered by a onFinalize event, which means:
When a new object (or a new generation of an existing object) is
successfully created in the bucket. This includes copying or rewriting
an existing object.
So, without rewriting/regenerating the existing images the Extension will not be triggered.
However, you could easily write your own Cloud Function that does the same thing but is triggered, for example, by a call to a specific URL (HTTPS cloud Function) or by creating a new document in a temporary Firestore Collection (background triggered CF).
This Cloud Function would execute the following steps:
Get all the files of your bucket, see the getFiles() method of the
Google Cloud Storage Node.js Client API. This method returns a
GetFilesResponse object which is an Array of File instances.
By looping over the array, for each file, check if the file has a
corresponding resized image in the bucket (depending on the way you
configured the Extension, the resized images may be in a specific
folder)
If a file does not have a corresponding resized image, execute the
same business logic of the Extension Cloud Function for this File.
There is an official Cloud Function sample which shows how to create a Cloud Storage triggered Firebase Function that will create resized thumbnails from uploaded images and upload them to the database URL, (see the last lines of index.js file)
Note : If you have a lot of files to treat, you should most probably work by batch, since there is a limit of 9 minutes for Cloud Function execution. Also, depending on the number of images to treat, you may need to increase the timeout value and/or the allocated memory of your Cloud Function, see https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation
In case someone need it. This is how I resized existing image.
const functions = require("firebase-functions");
const axios = require("axios");
const { Storage } = require("#google-cloud/storage");
const storage = new Storage();
// Don't forget to replace with your bucket name
const bucket = storage.bucket("projectid.appspot.com");
async function getAlbums() {
const endpoint = "https://api.mydomain.com/graphql";
const headers = {
"content-type": "application/json",
};
const graphqlQuery = {
query: `query Albums {
albums {
id
album_cover
}
}`,
};
const response = await axios({
url: endpoint,
method: "post",
headers: headers,
data: graphqlQuery,
});
if (response.errors) {
functions.logger.error("API ERROR : ", response.errors); // errors
if any
} else {
return response.data.data.albums;
}
}
function getFileName(url) {
var decodeURI = decodeURIComponent(url);
var index = decodeURI.lastIndexOf("/") + 1;
var filenameWithParam = decodeURI.substr(index);
index = filenameWithParam.lastIndexOf("?");
var filename = filenameWithParam.substr(0, index);
return filename;
}
function getFileNameFromFirestore(url) {
var index = url.lastIndexOf("/") + 1;
var filename = url.substr(index);
return filename;
}
const triggerBucketEvent = async () => {
bucket.getFiles(
{
prefix: "images/albums", // you can add a path prefix
autoPaginate: false,
},
async (err, files) => {
if (err) {
functions.logger.error(err);
return;
}
const albums = await getAlbums();
await Promise.all(
files.map((file) => {
var fileName = getFileNameFromFirestore(file.name);
var result = albums.find((obj) => {
return getFileName(obj.album_cover) === fileName;
});
if (result) {
var file_ext = fileName.substr(
(Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1
);
var newFileName = result.id + "." + file_ext;
// Copy each file on thumbs directory with the different name
file.copy("images/albums/" + newFileName);
} else {
functions.logger.info(file.name, " not found in album list!");
}
})
);
}
);
};
exports.manualGenerateResizedImage = functions.https.onRequest(async () => {
await triggerBucketEvent();
});

Upload multiple attachments via Jira API using UrlFetchApp.fetch

I am using google app script server side coding to upload multiple files using Jira Rest API. I have used urlFetchApp.fetch() method for it. The payload i am passing is file blob data with 'Content-Type: false' as i am unable to pass it as formData through server side coding.
With this approach i am able to pass single file at a time to Jira attachments API, I have to call Jira API for each file.
Below is sample code,
function updateJiraTicket(fileUploadArr, ticketId){
const jira_Api = getJiraInstanceKeys();
const JIRA_URL= jira_Api["URL"] + ticketId + "/attachments";
const JIRA_LOGIN = jira_Api["LOGINNAME"];
const JIRA_PASSWORD = jira_Api["PASSWORD"];
const PROJECT = jira_Api["PROJECT"];
//fileUploadArr format getting passed from file upload UI side control
//fileUploadArr = [{“fileData”:”{base64 file data}”, “fileType”:”image/jpg”,”fileName”:”testimg1”},{“fileData”:”{base64 file data}”, “fileType”:”image/jpg”,”fileName”:”testimg2”}]
fileUploadArr.forEach(function(arr) {
let file = getFile(arr.fileData, arr.fileType, arr.fileName);
let formdata = {'file' : file };
let params = {
method: 'POST',
headers: {
'Authorization': 'Basic ' + Utilities.base64Encode(JIRA_LOGIN+':'+JIRA_PASSWORD),
'Content-Type': false,
'X-Atlassian-Token': 'no-check'
},
"payload": formdata,
muteHttpExceptions: true
}
let result = UrlFetchApp.fetch(JIRA_URL, params);
let response = result.getContentText();
})
}
function getFile(data, type, name) {
var file = Utilities.newBlob(Utilities.base64Decode(data), type, name);
return file;
}
Can anyone help with this?

How to change an Apify actor parameter via API

I want to call an Apify actor and specify a parameter value via a call to the Apify API.
The actor is the Google Search Results Scraper located here.
Here is where the docs say to use queries as the object property name in the API call payload.
The following table shows specification of the actor INPUT fields as defined by its input schema. These fields can be [...] provided in a JSON object when running the actor using the API. Read more in docs.
...
Search queries or URLs
Google Search queries (e.g. food in NYC) and/or full URLs (e.g. https://www.google.com/search?q=food+NYC).
Enter one item per line.
Optional
Type: String
JSON example
"queries": "Hotels in NYC
Restaurants in NYC
https://www.google.com/search?q=restaurants+in+NYC"
After I run my Google Apps Script code, I expect to see a change in the searchQueries.term parameter to be the following.
Apify — what I expect to see
"searchQuery": {
"term": "Banks in Phoenix", // what I am trying to change to by API call
// [...]
},
But what I actually get is the same parameter value as existed the last time I ran the actor manually. As follows.
Apify — what I actually see
"searchQuery": {
"term": "CPA firms in Newark", // remaining from last time I ran the actor manually
// [...]
},
Here is the code I'm running from Google Apps Script.
Code.gs
const runSearch = () => {
const apiEndpoint= `https://api.apify.com/v2/actor-tasks/<MY-TASK-NAME>/run-sync?token=<MY-TOKEN>`
const formData = {
method: 'post',
queries: 'Banks in Phoenix',
};
const options = {
body: formData,
headers: {
'Content-Type': 'application/json',
},
};
UrlFetchApp.fetch(apiEndpoint, options,);
}
What am I doing wrong?
You are missing the payload property in the request object.
Change:
queries: 'Banks in Phoenix',
to:
payload: {
queries: 'Banks in Phoenix',
}
Code.gs
const runSearch = () => {
const apiEndpoint= `https://api.apify.com/v2/actor-tasks/<MY-TASK-NAME>/run-sync?token=<MY-TOKEN>`
const formData = {
method: 'post',
payload: {
queries: 'Banks in Phoenix',
},
};
const options = {
body: formData,
headers: {
'Content-Type': 'application/json',
},
};
UrlFetchApp.fetch(apiEndpoint, options,);
}

Generating zoho ticket using google apps script

I am trying to use zoho desk api in google apps script.
I am trying to genereate ticket through google script.But getting error.
Whereas if I do it in PHP its working fine.
Please find both codes for reference:
PHP CODE which is working
$auth_token = '12345ab';//your_auth_token
$org_id=12345; //your_organization_id
$ticket_data=array(
"departmentId"=>$getdepartmentid,
"contactId"=>$getcontactid,
"subject"=>$ticket_subject,
"description"=>$ticket_desc,
"priority"=>$priority,
"status"=>$ticketstatus,
"email"=>$contact_email,
"classification"=>$classification,
"channel"=>"Application"
);
$headers=array(
"Authorization: $auth_token",
"orgId: $org_id",
"contentType: application/json; charset=utf-8",
);
$url="https://desk.zoho.in/api/v1/tickets";
$ticket_data=(gettype($ticket_data)==="array")? json_encode($ticket_data):$ticket_data;
$ch= curl_init($url);
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,TRUE);
curl_setopt($ch,CURLOPT_POST,TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS,$ticket_data); //convert ticket data array to json
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response= curl_exec($ch);
$info= curl_getinfo($ch);
GOOGLE APPS SCRIPT(which is not working)
var authtoken = "12345ab"; //your_auth_token
var org_id=12345; //your_organization_id
var department=23220000000057620; // ID of department
var contact=23220000000066959; //ID of customer
var subject=location_urgent_inbox_folder_name + ' /' + Name_of_file_to_be_attached;
var description="Ticked via drive";
var status="open";
const ticketData = {
subject: subject,
departmentId: department, // Replace this with whatever yours is.
contactId: contact, // Replace this with whatever yours is.
description: description,
status: status
};
const zohoUrl = 'https://desk.zoho.in/api/v1/tickets';
try {
var response = UrlFetchApp.fetch(zohoUrl, {
"method": 'POST',
"muteHttpExceptions": true,
"headers": {
Authorization: authtoken,
orgId: org_id,
contentType: 'application/json',
},
"payload": JSON.stringify(ticketData),
});
Logger.log(response.getContentText());
const parsed = JSON.parse(response.getContentText());
} catch (error) {
Logger.log(error.toString());
}
Assuming I am looking at the right section of the right documentation (https://desk.zoho.com/DeskAPIDocument#Tickets_Createaticket), I think these are your issues:
Server's JSON-encoded response contains no message property, hence Logger.log(result.message); logs undefined. (Maybe trying logging response.getContentText() to see what properties are available in your case -- or refer to the API documentation.)
Authorization and orgId headers are missing in the request you send. (Looks like authtokens are deprecated (https://desk.zoho.com/DeskAPIDocument#Authentication) and you instead need to use OAuth 2.0 (https://desk.zoho.com/DeskAPIDocument#OauthTokens).)
Data needs to be sent in request's body. (You appear to be sending it in the query string.)
I've not read the documentation in detail but I don't see any mention of query string parameters authtoken and JSONString (CTRL+F returns no matches). So you might want to get rid of them in your code and instead follow what the documentation says.
The code below is untested and won't work (as you need to replace with your own credentials). But it should give you an idea of how you can accomplish this.
// See: https://desk.zoho.com/DeskAPIDocument#Tickets#Tickets_Createaticket
// Required: subject, departmentId, contactId
const ticketData = {
subject: location_urgent_inbox_folder_name + ‘ /’ + Name_of_file_to_be_attached, // Taken from your question. Presume these are declared, valid and in scope.
departmentId: '12345', // Replace this with whatever yours is.
contactId: '12345', // Replace this with whatever yours is.
description: 'Ticked via drive',
status: 'open'
};
const zohoUrl = 'https://desk.zoho.in/api/v1/tickets';
try {
// See: https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchurl,-params
const response = UrlFetchApp.fetch(zohoUrl, {
method: 'POST',
contentType: 'application/json',
headers: {
orgId: '12345' // Replace this with whatever yours is.
Authorization: 'Zoho-oauthtoken ....' // See: https://desk.zoho.com/DeskAPIDocument#OauthTokens
},
payload: JSON.stringify(ticketData),
});
Logger.log(response.getContentText());
const parsed = JSON.parse(response.getContentText());
} catch (error) {
Logger.log(error.toString());
}
I've never worked with any of Zoho's APIs and haven't worked with Google Apps Script for a while, so apologies if I'm missing something.
Issue resolved,
Posting answer so that it might help someone
//create ticket in zoho
var authtoken = "abcd"; //your_auth_token
var org_id="12345"; //your_organization_id
var department="54321"; // ID of department in which ticket to be raised
var contact="9999"; //ID of customer
var subject="Ticket via google script";
var description="Ticket via google script";
var status="open";
const ticketData = {
subject: subject,
departmentId: department, // Replace this with whatever yours is.
contactId: contact, // Replace this with whatever yours is.
description: description,
status: status
};
const zohoUrl = 'https://desk.zoho.in/api/v1/tickets';
try {
var response = UrlFetchApp.fetch(zohoUrl, {
"method": 'POST',
"muteHttpExceptions": true,
"headers": {
Authorization: authtoken,
orgId: org_id,
contentType: 'application/json',
},
"payload": JSON.stringify(ticketData),
});
const parsed = JSON.parse(response.getContentText());
} catch (error) {
Logger.log(error.toString());
}