Calling an API endpoint upon changes to any Google Sheets files - google-apps-script

What I wish to achieve:
Whenever a cell is changed in any google sheet on my shared drive (by
any user on the domain) I want to call an API endpoint and include
information about which cell was edited.
My approach:
I believe Google App Scripts Add-on is what I need. Installed for all users on the domain.
I see there are "bound" scripts and standalone scripts. For standalone scripts I am not able to create any other triggers than timer and calender based triggers. Bound scripts seem to be permanently bound to a single sheet and won't impact other sheets in any way.
What am I missing?
I find a few end-to-end tutorials on blogs for making bound scripts, but nothing for generic cross-domain stuff.

You can achieve all this through a standalone script. Create a standalone script and follow these steps:
Step 1: Get spreadsheet ids
First you would have to get the id of the different Spreadsheets in your shared drive. You can do it in Google Apps Script itself if you use the Advanced Drive Service (see Reference below). To activate this service, go to Resources > Advanced Google services... in your script editor and enable Drive API.
Then, write a function that will return an array of the spreadsheet ids in the shared drive. You will have to call Drive.Files.list for that. It could be something along the following lines (please write your shared driveId in the corresponding line):
function getFileIds() {
var params = {
corpora: "drive",
driveId: "your-shared-drive-id", // Please change this accordingly
includeItemsFromAllDrives: true,
q: "mimeType = 'application/vnd.google-apps.spreadsheet'",
supportsAllDrives: true
}
var files = Drive.Files.list(params)["items"];
var ids = files.map(function(file) {
return file["id"];
})
return ids;
}
Step 2: Create triggers for each spreadsheet
Install an onEdit trigger programmatically for each of the spreadsheets (an edit trigger fires a function every time the corresponding spreadsheet is edited, so I assume this is the trigger you want). For this, the ids retrieved in step 1 will be used. It could be something similar to this:
function createTriggers(ids) {
ids.forEach(function(id) {
var ss = SpreadsheetApp.openById(id);
createTrigger(ss);
})
}
function createTrigger(ss) {
ScriptApp.newTrigger('sendDataOnEdit')
.forSpreadsheet(ss)
.onEdit()
.create();
}
The function createTriggers gets an array of ids as a parameter and, for each id, creates an onEdit trigger: everytime any of these spreadsheets is edited, the function sendDataOnEdit will run, and that's where you will want to call your API endpoint with information about the edited cell.
Step 3: Call API endpoint
The function sendDataOnEdit has to get data from the edited cell and send it somewhere.
function sendDataOnEdit(e) {
// Please fill this up accordingly
var range = e.range;
var value = range.getValue();
UrlFetchApp.fetch(url, params) // Please fill this up accordingly
}
First, it can get information about the cell that was edited via the event object, passed to the function as the parameter e (you can get its column, its row, its value, the sheet and the spreadsheet where it is located, etc.). For example, to retrieve the value of the cell you can do e.range.getValue(). Check the link I provide in reference to get more details on this.
Second, when you have correctly retrieved the data you want to send, you can use UrlFetchApp.fetch(url, params) to make a request to your URL. In the link I provide below, you can see the parameters you can specify here (e.g., HTTP method, payload, etc.).
Please bear in mind that you might need to grant some authorization to access the API endpoint, if this is not public. Check the OAuth reference I attach below.
(You have to edit this function accordingly to retrieve and send exactly what you want. What I wrote is an example).
Summing this up:
In order to create the triggers you should run createTriggers once (if you run it more times, it will start creating duplicates). Run for example, this function, that first gets the file ids via Drive API and then creates the corresponding triggers:
function main() {
var ids = getFileIds();
createTriggers(ids);
}
Also, it would be useful to have a function that will delete all the triggers. Run this in case you want to start from fresh and make sure you don't have duplicates:
function deleteTriggers() {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(trigger) {
ScriptApp.deleteTrigger(trigger);
})
}
Reference:
Advanced Drive Service
Drive.Files.list
onEdit trigger
Install trigger programmatically
onEdit event object
UrlFetchApp.fetch(url, params)
Connecting to external APIs
OAuth2 for Apps Script
ScriptApp.deleteTrigger(trigger)
I hope this is of any help.

Related

How to call an external database google spreadsheet and overcome permissions via spreadsheetApp.openById() GAS

I've been searching around for ages to find a solution to this problem.
There are two spreadsheet files that I own. 1- Recipe 2- Database
I want to pull the cost and volume of the ingredients from the database spreadsheet to inform the recipe spreadsheet and to automatically update when the database changes.
The error persists:
Exception: You do not have permission to call SpreadsheetApp.openById. Required permissions: https://www.googleapis.com/auth/spreadsheets
I realise that I cannot use SpreadsheetApp.openById() as it cannot call an external spreadsheet, that requires a function that allows permissions.
I've tried adding a custom menu item to ping the permissions check but that didn't cure the problem. I have also tried installing triggers that have solved the problem for other users, but to no avail. I have also adjusted the appsscript.json file to include the oauth scopes that are quoted in the error message but that hasn't changed the result. I have also made sure that the permissions on the files themselves are set to "anyone with the link can edit". The Google Sheets API is also installed.
There might be a glaring error in my code (I'm still pretty new to all this) that you fine folk might be able to point out to me.
function myonOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Database')
.addItem('Access', 'accessDB')
.addToUi();
}
function accessDB() {
SpreadsheetApp.openById('1JvQGDBY2Du0rtbHcQRdteLEGcWCSuS2JO7YinaC-tDc');
SpreadsheetApp.getUi().alert('Database now active!');
}
Which then informs:
function onOpen() {
ScriptApp.newTrigger('myOnEdit')
.onEdit()
.create();
}
/**
* This function calls the database to give permission
*/
function myOnEdit(e){
SpreadsheetApp.openById("1JvQGDBY2Du0rtbHcQRdteLEGcWCSuS2JO7YinaC-tDc");
}
/**
* This fuction determines whether the ingredient is in the database & returns it's price point and breaks
* #constructor
* #customfunction
* */
function getCost(itemname){
const ss1 = SpreadsheetApp.openById("1JvQGDBY2Du0rtbHcQRdteLEGcWCSuS2JO7YinaC-tDc");
const ssh = ss1.getSheetByName('Cost');
let tf = ssh.getRange('A1:A'+ ssh.getLastRow()).createTextFinder(itemname).findNext();
return ssh.getRange(tf.getRow(),3).getValue();
}
If I'm barking up the wrong proverbial tree here and my custom functions just simply can't call external spreadsheets, what would be the workaround?
thanks in advance.
For trigger, instead of creating trigger by code, try installing it in the Triggers tab in your Apps Script. Once you save the trigger, it will pop up an authentication window, accept it and it will add the necessary scope to your project.
Triggers Tab:
This is what your trigger setup should looks like:
The openById() works and I tested it by creating 2 dummy spreadsheet, created onEdit() trigger in the 1st spreadsheet that prints the values in the 2nd spreadsheet.
2nd spreadsheet:
Edited cell A1 in 1st spreadsheet:
Code in 1st spreadsheet:
function myOnEdit(e){
var ss = SpreadsheetApp.openById("2nd spreadsheet id");
var sh = ss.getSheetByName("Sheet1");
var data = sh.getRange("A1:A3").getValues();
Logger.log(data);
}
Output:
Reference:
Installable Trigger

How do I use the drive.file scope for a standalone google apps script

I have a standalone script that uses the Sheets API. There are only two calls:
SpreadsheetApp.openById(spreadsheetID).getSheetByName("Answers")
SpreadsheetApp.openById(otherspreadsheetID).getSheetByName("Questions").getDataRange().getValues()
So it reads from one file and writes to a different file. The script is set to run as a webapp as me, so on initial run, by default, this triggers a broad scope to view/edit/delete all sheets. I want to limit that. I see this is a scope I could manually set: https://www.googleapis.com/auth/drive.file (docs).
But I don't get how to then set my two spreadsheets as files that have been "opened or created" by the app so that the scope is valid for those files. I tried creating a function that creates two new sheets (I figured this counts as "a drive file that you created with this app") but even the Spreadsheetapp.create() function throws a "do not have permission" error.
I might be misunderstanding how this scoping works?
Issue and workaround:
When SpreadsheetApp.openById() is used, the scope of https://www.googleapis.com/auth/spreadsheets is used. It seems that this is the current specification.
So, as a workaround, how about using Sheets API? When the Sheets API is used, you can use the scope of https://www.googleapis.com/auth/drive.file.
As an important point, the official document of the scope of https://www.googleapis.com/auth/drive.file is as follows.
Per-file access to files created or opened by the app. File authorization is granted on a per-user basis and is revoked when the user deauthorizes the app.
So, when you want to retrieve the data from the Spreadsheet using the scope of https://www.googleapis.com/auth/drive.file, at first, the Spreadsheet is required to be created by the scope. In this answer, I would like to introduce the following flow.
Set the scope of https://www.googleapis.com/auth/drive.file to Google Apps Script project.
Create a new Spreadsheet.
Retrieve values from the Spreadsheet.
Usage:
1. Set scope.
Please create a new Google Apps Script. From your question, I understood that you are using the standalone type. For this, please set the scope of https://www.googleapis.com/auth/drive.file to the manifest file. Ref Please add "oauthScopes": ["https://www.googleapis.com/auth/drive.file"] to the file of appsscript.json. By this, the specific scope can be used.
2. Create a new Spreadsheet.
Before you use this script, please enable Sheets API at Advanced Google services. When you run the following script, a new Spreadsheet is created and you can see the spreadsheet ID at the log. Please copy the ID. This ID is used in the next script. By this, a new Spreadsheet is created by the scope of https://www.googleapis.com/auth/drive.file.
function createNewSpreadsheet() {
const id = Sheets.Spreadsheets.create({properties: {title: "sample"}}).spreadsheetId;
console.log(id)
}
When you try to retrieve the values from the existing Spreadsheet which is not created by this Google Apps Script project, an error of Requested entity was not found. occurs. So in this section, a new Spreadsheet is created by this Google Apps Script project.
3. Get values from Spreadsheet.
Before you use this script, please open the created Spreadsheet and check the sheet name. And, as a sample, please put the sample values to the cells. When you run the following script, the values are retrieved from the Spreadsheet and you can see them at the log.
function getValues() {
const spreadsheetId = "###"; // Please set the spreadsheet ID.
const obj = Sheets.Spreadsheets.Values.get(spreadsheetId, "Sheet1");
const values = obj.values;
console.log(values)
}
References:
Manifest structure
Method: spreadsheets.create
Method: spreadsheets.values.get

Google Forms needing post processing writing to a Google Sheet

We're using a Google Form which writes to a Google Spreadsheet. I would like to do some post processing of the data as it enters the datasource.
I have the following code started. Is there a way to implicity obtain a reference to the active spreadsheet or do I need to modify it to have a hard reference to the id?
Is it possible to intercept a datastream and modify values before arriving at the Google Sheet?
function onFormSubmit(event) {
// This function will be called everytime the form is submitted.
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Form Responses 1').getRange(SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Form Responses 1').getLastRow(), 4).setBackground('#4287f5');
}
Solution:
You simply need to take advantage of the event object:
function onFormSubmit(event) {
const ss = event.source;
const as = ss.getActiveSheet();
if (as.getName()=='Form Responses 1'){
as.getRange(as.getLastRow(),4).setBackground('#4287f5');
}
}
Please Note:
There are two onFormSubmit triggers: one for google sheets and one for google forms. In your case, since you want to edit something in the spreadsheet, it makes more sense to use the first one.
Both are installable triggers which means that you need to install it either by a script or manually. To install it manually you can click on project's triggers, create a trigger, select onFormSubmit as the function and event type On form submit.

Google Sheets, "The request is missing a valid API key"

I am trying to use the Google Sheets API. The problem is, once I call my script function on the google spreadsheet, I get the following error:
API call to sheets.spreadsheets.values.get failed with error: The request is missing a valid API key. (line 5).
where line 5 in the script looks like this:
var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;
and spreadsheetId and rangeName are defined in the first lines.
I think the problem might be that I did not copy the API key anywhere, but I really do not understand where and how I can do it.
I call the function just using = function().
When you use Sheets API by a custom function like =myFunction() put to a cell, such error occurs. When the custom function is run, ScriptApp.getOAuthToken() returns null. I think that this is the mainly reason of your issue. And unfortunately, I think that this is the specification. In order to avoid this issue, I would like to propose 2 workarounds.
Workaround 1:
A part of Spreadsheet Services can be used at the custom function. So using this, it obtains the same result with var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;. In the case of your script, openById() cannot be used. So the script is as follows.
Sample script:
function customFunc() {
var rangeName = "#####"; // Please set this.
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var values = sheet.getRange(rangeName).getValues();
return values;
}
Workaround 2:
If you want to use Sheets API, the access token is required. But it is required to directly request to the endpoint of Sheets API, because the access token is automatically used in internal at Advanced Google Services. As an issue of this case, there is that when ScriptApp.getOAuthToken() is run in the custom function, null is returned. In order to avoid this, as a method, the access token is directly given to the custom function using PropertiesService. The sample flow is as follows.
When the Spreadsheet is opened, it puts the access token to PropertiesService by the OnOpen trigger.
When you use this, please install the OnOpen trigger to onOpenFunc() in the sample script.
When the custom function is run, the access token retrieved by PropertiesService is used for using Sheets API.
By this, Sheets API can be used in the custom function.
Sample script:
// Please install OnOpen trigger to this function.
function onOpenFunc() {
PropertiesService.getScriptProperties().setProperty("accessToken", ScriptApp.getOAuthToken());
}
function customFunc() {
var spreadsheetId = "#####"; // Please set this.
var rangeName = "#####"; // Please set this.
var accessToken = PropertiesService.getScriptProperties().getProperty("accessToken");
var url = "https://sheets.googleapis.com/v4/spreadsheets/" + spreadsheetId + "/values/" + rangeName;
var res = UrlFetchApp.fetch(url, {headers: {"Authorization": "Bearer " + accessToken}});
var obj = JSON.parse(res.getContentText());
var values = obj.values;
return values;
}
The expilation time of access token is 1 hour. In this sample script, PropertiesService is used. In this case, when 1 hour is spent after the Spreadsheet was opened, the access token cannot be used. If you want to use continuously the access token, you can also update it using the time-driven trigger.
Note:
When you use Sheets API, please enable Sheets API at API console.
References:
Custom Functions in Google Sheets
spreadsheets.values.get
PropertiesService
If these workarounds were not what you want, I apologize.
I want to thank you, #Tanaike, for your response (I don't have enough 'points' to upvote or comment, so my only option is an 'Answer')
I know this thread is several years old, but I thought others might be interested in my personal experience.
First of all: "Workaround 1" worked for me!
The function/method "Sheets.Spreadsheets.Values.get(spreadsheetID, RangeName).values" was giving me an "missing a valid API key" error, so I swapped it for "sheet.getRange(RangeName).getValues()".
Most of the above was set as Global Variables, i.e. outside of any functions.
Weird thing was that this error occured only when running from within the [container] sheet, not from the "embedded" script.
For instance: If I had an active onOpen() function, and I opened/refreshed the Sheet, the script would log a "The request is missing a valid API key." error, and the UI/Menu I had built therein would not load.
I could, however run the onOpen() function from within the script itself, and the menu would appear, and function, within the Sheet. If I disabled/renamed the onOpen() function, and reloaded the Sheet, I would not get the error message.
Makes sense, as the simple loading of the Sheet does not appear to run the script, but when one does access it (the script), i.e. through the onOpen() function, then the initial global variables are read (and the error occurs).
However, when I ran the same function, or others, from within the script itself, they would run ok. This permissions conundrum is what has led me on a wild goose chase all over the Internet, ultimately landing here.
All this after numerous other issues, in the course of whose resolution I built a Google Cloud Project, added APIs (e.g. for Sheets), added scopes to the oauthScopes section of the manifest, and more.
It was only after I made the replacements described above that everything worked, both from the script, and its container spreadsheet! So, THANKS!
Anyway... Sorry for the long post, but I hope others may benefit from your solution, and in which context it helped me.
Cheers,
-Paul

Error: no permission to append row

I have the following google script. Trying to append the results to row and I get this error
"You do not have permission to call appendRow (line 11)."
function webs() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sheet = ss.getActiveSheet();
var url = "https://api.pipedrive.com/v1/mailbox/mailMessages/17685?include_body=1";
var token = "&api_token=token"
var response = UrlFetchApp.fetch(url+token);
var dataSet = JSON.parse(response.getContentText());
sheet.appendRow([dataSet.data.body]);
}
Anyway around this? if I can't append in google sheets, is there anyway to add results to a row?
I believe you need to use an Installable Trigger instead of using a Simple Trigger which have restrictions on what they can do.
See how to install it manually here:
From the script editor, choose Edit > Current project's triggers.
Click the link that says: No triggers set up. Click here to add one now.
Under Run, select the name of function you want to trigger.
Under Events, select either Time-driven or the Google App that the script is bound to (for example, From spreadsheet).
Select and configure the type of trigger you want to create (for example, an Hour timer that runs Every hour or an On open trigger).
Optionally, click Notifications to configure how and when you will be contacted by email if your triggered function fails.
Click Save.
Edit: I could also be restrictions on custom functions:
A custom function cannot affect cells other than those it returns a value to. In other words, a custom function cannot edit arbitrary cells, only the cells it is called from and their adjacent cells. To edit arbitrary cells, use a custom menu to run a function instead.
Edit: Other times you just need to authorize it first. Try going to Run > Run Function > then choose any function to run. Apps Script will pop-up with a authorization window for all the new scopes you are using.