How to Access ScriptDB from Spreadsheet Custom Function? - google-apps-script

In this post, I suggested to use the ScriptDB as an intermediate storage for global data of a Container Extension code. I wrote a sample code for my answer but the sample throws the error: You do not have permission to call query (line X) exception in a ScriptDb.getMyDb().query(...); line. I created the following simpler example demonstrating the problem. The code, both getDBSize and getSource functions, is permitted to use the ScriptDB by running it in the editor. The getDBSize function is executed without any problem by pressing the Run button in the Spreadsheet Script Manager Dialog. The getSource function works everywhere.
I published the Spreadsheet for the example - link. It is impossible to share the code for view, but it is possible to output it in a cell, the cell B3 contains exactly bellow code.
How is possible to permit the Spreadsheet Code to have access to the ScriptDB?
function getDBSize() {
var db = ScriptDb.getMyDb();
var result = db.query({});
var count = result.getSize();
return count;
}
function getSource() {
return this.toSource();
}

The problem is that you're trying to run this function as a spreadsheet custom function, and custom functions are way more limited than all examples on the Container Extension page you linked.
But, from a theoretical point of view, custom functions as well as simple event handlers (e.g. onEdit, onOpen), can not access anything that requires the user account or is associated with the user, only "generic" methods. For example, UrlFetchApp, open the current spreadsheet, or read ScriptProperties.
So, in thesis, querying a ScriptDb should be possible, since it's a generic call and has nothing to do with the active user, it's analogous to ScriptProperties. I don't see a workaround that would actually let you query the db, but you could use ScriptProperties instead. You can easily save and retrieve any object you would save on ScriptDb by using JSON.stringify and .parse. Of course, I'm not comparing ScriptDb capabilites with ScriptProperties, it's just a workaround.
Anyway, this seems like a good candidate for an enhancement request on our issue tracker.

Related

Why is doGet() failing without posting logs?

I suppose my question is twofold: doGet() in the following context will just fail after 0.1~0.2 seconds without posting logs, so I have no idea how to troubleshoot it by myself. Additionally, if I'm having the script execute on my behalf, do I have to push a request with my authorization token to a more "pertinent" area than just the sheet name, such as within the iteration itself? Read further for more details:
I have a source spreadsheet where I am cross-referencing user inputted data to validate the information we have "on file". Most of our clients are over the age of 55, so I am trying to reduce end-user complexity by having the script run on my behalf whenever they need to use it (to bypass the Authorization screen, with the big scary "This application could be unsafe!" message). The way I've read to accomplish this seems to be with doGet(), so I set up a low-level HTTP Get request that just pushes a doGet() with my OAuth token, returning the sheet name. I also set up a masking function specifically to do this, and linked it to the button originally used for the iteration logic. The doGet() looks like this:
const doGet = e => {
Logger.log(`Recieved HTTP request.`);
const content = ContentService.createTextOutput(iterator(e));
Logger.log(content);
return content;
}
and the button that uses UrlFetchApp looks like:
const runMask = () => {
const active = SpreadsheetApp.getActiveSheet().getSheetName();
const v4 = 'https://script.google.com/macros/s/<scriptid>/dev' // ScriptApp.getService().getUrl() posts 404
UrlFetchApp.fetch(`${v4}?sheetName='${active}'`, {
headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` },
});
I have some logs set up within the real runMask() that proceed all the way to the end of the program, giving me real URLs and OAuth tokens, so I know it's making it through runMask() without an issue. However, the doGet() log doesn't post anything, even at the top of the function. I can see that it's executing the trigger in my execution log, but the log itself remains empty.
I've tried:
using ScriptApp.getService().getUrl() in place of v4: posts 404 in the log w/ truncated server response
replacing ${active} with the name of the sheet: same issue; logging ${active} also returns the correct name of the sheet.
Beyond this, I'm not even sure what to do. I have everything scoped correctly (auth/spreadsheets.currentonly, auth/script.external_request, and auth/userinfo.email), and I have no issues about operational security (as both the spreadsheet and script are written by me, the clients have no need to grant access to their entire drive). Before trying to implement doGet() and bypass the authorization screen, the iterator itself worked just fine. As such, I have chosen not to include it here, as it's hardly relevant (the function that executes the iteration function never makes it to that point).
I understand this has been quite the deluge of information; I'd be happy to provide more information or context as needed.
Getting ReferenceError: iterator is not defined (line 12, file "ag2")
With this:
const doGet = e => {
Logger.log(`Recieved HTTP request.`);
const content = ContentService.createTextOutput(iterator(e));
Logger.log(content);
return content;
}
Issued with url/exec?option=A
It runs with
const doGet = e => {
Logger.log(`Recieved HTTP request.`);
const content = ContentService.createTextOutput(JSON.stringify(e));
Logger.log(content);
return content;
}
and returns the appropriate stringified object
Only use the test URL (/dev) for testing the web app from a web browser.
Before doGet from a web browser using a versioned deployment (/exec) remember to publish a new version.
Assign a Google Cloud Project to your Google Apps Script project. For details see https://developers.google.com/apps-script/guides/cloud-platform-projects.
To make it easier to debug your avoid calling functions from a Google Apps Script method like createTextOutput, instead, assign the function result to a variable and use it as the method parameter, i.e. replace
const content = ContentService.createTextOutput(iterator(e));
by
const something = iterator(e);
const content = ContentService.createTextOutput(something);
For debugging purposes, create a function to call your doGet function, and check that it hasn't any problem to run, i.e.
function __test__doGet(){
const e = {
parameter: {}
}
doGet(e);
}
Related
Exception handling in google apps script web apps
Issue:
When I saw your question, I'm worried about I have everything scoped correctly (auth/spreadsheets.currentonly, auth/script.external_request, and auth/userinfo.email).
If you are using only the following scopes at oauthScopes of appsscript.json,
https://www.googleapis.com/auth/spreadsheets.currentonly
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/userinfo.email
Unfortunately, these scopes cannot be used for access to Web Apps. Although I'm not sure about the method for running your function of runMask, I thought that this might be the reason for your issue.
Solution:
If you want to access Web Apps of https://script.google.com/macros/s/<scriptid>/dev using the access token retrieved by ScriptApp.getOAuthToken(), please include the following scope.
https://www.googleapis.com/auth/drive.readonly
or
https://www.googleapis.com/auth/drive
After you include the above scope, please reauthorize the scopes, and test it again. When your function of iterator has already been declared and the script worked, by running runMask, you can see the log of Logger.log(Recieved HTTP request.) and Logger.log(content) at the log.
Reference:
Taking advantage of Web Apps with Google Apps Script

Try Catch for if script is already running in sheets, prevent a 2nd instance at same time?

I have a script that does some basic copy and pasting of data, creates a sheet as a pdf and attaches to an email.I have a button to start it for my less-tech-experienced colleagues embedded in a sheet:
We share the spreadsheet doc and I'd like to keep it that way for version control. What's the best way to set the script up so that if an instance of the script is already running in the spreadsheet doc it rejects a 2nd attempt until the first one is completed? Some sort of try catch?
I don't think the code is as relevant for this question, here's the beginning of the function in question:
function failedSettlementsEmailz() {
var contacts;
var toastMembers="";
....more code...
It calls two other functions in my script document that is bound to the Google Sheet spreadsheet.
Thanks
Take a look at the LockService.
For example in your code have:
var lock = LockService.getScriptLock();
if (lock.tryLock(1000)) { // Wait for 1s
// Do stuff ...
lock.releaseLock()
} else {
// This script is already running, try again later ...
}
I understood that when the script is run by clicking a button, you don't want to run the script more while the script is already running. If my understanding is correct, how about this workaround? Please think of this as one of several answers.
In this workaround, I selected to use CacheService. "CacheService allows you to access a cache for short term storage of data." The default expiration is 10 minutes. This is over the maximum execution time of GAS (6 minutes). So I selected this.
Flow :
Retrieve the key of "script" from CacheService.
If there is the key, it means that the script is running.
If there is no key, it means that the script is not running.
At this time, put the key of "script" with a value.
The process you want is run.
After the process was finished, the key is removed. This means the script is not running.
Sample script :
function failedSettlementsEmailz(){
var cache = CacheService.getScriptCache();
if (!cache.get("script")) {
cache.put("script", "running"); // you can use various value for the value of "running".
// do something
cache.remove("script");
}
}
Reference :
CacheService
If I misunderstand your question, I'm sorry.

Autocomplete Not Working - Google App Script

I'm having trouble with the autocomplete feature in Google App Script.
Built-in methods like SpreadsheetApp. will provide an autocomplete menu with methods to choose from.
However, if I create my own child object, autocomplete works for a little while, and then it just stops working.
for example:
var skywardRoster = SpreadsheetApp.getActiveSheet();
skywardRoster. will produce method options for a while, and then it stops.
However, the code still functions, and methods work if I type them out manually, so I know the declarations must be right. The menu simply won't appear, and it's just very inconvenient to have to look up each method individually as I go.
I have tried: breaking the variable and retyping that line; copy and pasting the code back into the editor; using different browsers; copying the gs file itself and working within the copy; and signing out completely and signing back in. Nothing seems to get it back to work.
I'm really new to coding, and I'm not sure what can be causing this.
Does anyone know how to fix this issue?
You might want to check Built-in Google Services:Using autocomplete:
The script editor provides a "content assist" feature, more commonly called "autocomplete," which reveals the global objects as well as methods and enums that are valid in the script's current context. To show autocomplete suggestions, select the menu item Edit > Content assist or press Ctrl+Space. Autocomplete suggestions also appear automatically whenever you type a period after a global object, enum, or method call that returns an Apps Script class. For example:
If you click on a blank line in the script editor and activate autocomplete, you will see a list of the global objects.
If you type the full name of a global object or select one from autocomplete, then type . (a period), you will see all methods and enums for that class.
If you type a few characters and activate autocomplete, you will see all valid suggestions that begin with those characters.
Since this was the first result on google for a non-working google script autocompletion, I will post my solution here as it maybe helps someone in the future.
The autocompletion stopped working for me when I assigned a value to a variable for a second time.
Example:
var cell = tableRow.appendTableCell();
...
cell = tableRow.appendTableCell();
So maybe create a new variable for the second assignment just during the implementation so that autocompletion works correctly. When you are done with the implementation you can replace it with the original variable.
Example:
var cell = tableRow.appendTableCell();
...
var replaceMeCell = tableRow.appendTableCell(); // new variable during the implementation
And when the implementation is done:
var cell = tableRow.appendTableCell();
...
cell = tableRow.appendTableCell(); // replace the newly created variable with the original one when you are done
Hope this helps!
I was looking for a way how to improve Google Apps Script development experience. Sometimes autocomplete misses context. For example for Google Spreadsheet trigger event parameters. I solved the problem by using clasp and #ts-check.
clasp allows to edit sources in VS Code on local machine. It can pull and push Google Apps Script code. Here is an article how to try it.
When you move to VS Code and setup environment you can add //#ts-check in the beginning of the JavaScript file to help autocomplete with the special instructions. Here is the instructions set.
My trigger example looks like this (notice autocompletion works only in VS Code, Google Apps Script cloud editor doesn't understand #ts-check instruction):
//#ts-check
/**
* #param {GoogleAppsScript.Events.SheetsOnEdit} e
*/
function onEditTrigger(e) {
var spreadsheet = e.source;
var activeSheet = spreadsheet.getActiveSheet();
Logger.log(e.value);
}
I agree, Google Script's autocomplete feature is pretty poor comparing with most of other implementations. However the lack is uderstandable in most cases and sometimes the function can be preserved.
Loosing context
The autocomplete is limited to Google objects (Spreasheets, Files, etc.). When working with them you get autocomplete hints, unless you pass such object instance to function as an argument. The context is lost then and the editor will not give you suggestions inside the called function. That is because js doesn't have type control.
You can pass an id into the function instead of the object (not File instance but fileId) and get the instance inside of the function but in most cases such operation will slow the script.
Better solution by Cameron Roberts
Cameron Roberts came with something what could be Goole's intence or a kind of hack, don't know. At the beginning of a function assign an proper object instance to parameter wariable and comment it to block:
function logFileChange(sheet, fileId){
/*
sheet = SpreadsheetApp.getActiveSheet();
*/
sheet.appendRow([fileId]); // auto completion works here
}
Auto completion preserved

Can lockservice for GAS work across multiple functions in the same project

I had a problem with a script I wrote, the solution to the issue was lock service to avoid collisions with form submits. As I had no idea this would be an issue, I've had to go back and revisit old scripts.
I have script that has a few different functions, and it passes data from one function to another. Eventually it writes data to sheet, creates a PDF, can email it and stores the PDF to a folder in google drive.
Here's a brief example of what I mean
function firstFunction() {
//Do stuff, return something
return something;
secondFunction(something);
}
function secondFunction(something) {
// Do stuff, return test
thirdFunction(test);
}
function thirdFunction(test) {
// Do stuff, return that
return that;
fourthFunction(that);
}
function fourthFunction(that){
// finish doing stuff. Write data
}
I also have a separate script, that would invoke the first and iterate through a list of data, to bulk produce PDFs.
I'm worried that if 2 people invoke the script at the same time, I'll have issues again.
Given the example script I've given, Do I have to use LockService on each function? Or can I declare the lock in the first function and release it in the last.
I'm also curious on how it would sit with the 2nd script that invokes the first several times. Would adding the lock service in this one be sufficient, or would I also have to add it to the second too?
Thanks in advance.
EDIT BELOW
I just remembered I posted the real code on Code Review for advice, and boy did I get some!!
Code Review Post
I should think that you don't need Lock Service in this case at all.
In the Lock Service documentation it states:
[Lock Service] Prevents concurrent access to sections of code. This can be useful when you have multiple users or processes modifying a shared resource and want to prevent collisions. (documentation: https://developers.google.com/apps-script/reference/lock/lock-service#getScriptLock%28%29)
or [Class Lock] is particularly useful for callbacks and triggers, where a user action may cause changes to a shared resource and you want to ensure that aren't collisions. (documentation: https://developers.google.com/apps-script/reference/lock/lock#tryLock%28Integer%29)
Now, having read the script code that you link to in your edit, I saw no shared resources that the script is writing to. So I conclude no lock is required. (EDIT: On second reading, I see that the script is writing to a sheet once, the shared resource. So your lock can go within that function only.)
I will cross post this point to Google Apps Script Plus community https://plus.google.com/communities/102471985047225101769 since there are experts there who can confirm.

Service Invoked Too Many Times (Google Apps Script)

I want to use Google Apps Script to make custom functions for a spreadsheet. I've made an extremely simple function:
function foo(){
return "bar";
};
The problem is that I need this function in a couple hundred cells. When I paste the function =foo() into all of these cells, the function works in a few of the cells, but in most I get this error: "Service invoked too many times: spreadsheet. Try Utilities.sleep(1000) between calls."
[Screenshot here]
I guess I don't understand why this function, simple as it is, is considered an invocation of the Spreadsheet Services. I'm not even requesting any data (except for the function itself). Is that the problem? And if so, is there a workaround? Custom functions could make Google Spreadsheets infinitely more powerful, but this problem hamstrings the possibility of using a custom function in multiple cells. Suggestions?
(P.S. -- Using the Utilities.sleep() function as suggested by the error message doesn't help at all when all of the cells call their functions simultaneously; it only slows the rate at which individual cells repeatedly call the function.)
According to the Optimization section on the Apps Script Function Guide:
Each time a custom function is used in a spreadsheet, Google Sheets makes a separate call to the Apps Script server. If your spreadsheet contains dozens (or hundreds, or thousands!) of custom function calls, this process can be quite slow.
Consequently, if you plan to use a custom function multiple times on a large range of data, consider modifying the function so that it accepts a range as input in the form of a two-dimensional array, then returns a two-dimensional array that can overflow into the appropriate cells.
To do this, pass in an input that represents the size of the array you'd like to return. When you start executing your function check if the input parameter is an array with input.map. If it is, you can call the the function on each item and return that entire collection.
So in your case like this:
function foo(){
return "bar";
};
You can update the function like this:
function foo(input){
if (input.map) { // Test whether input is an array.
return input.map(foo); // Recurse over array if so.
} else {
// do actual function work here
return "bar";
}
};
And then call it like this:
By calling the function in the spreadsheet, you are invoking the Spreadsheet service by asking it to go round-trip to the server to run the results of your function. As a result, you have make a couple hundred requests in a very short period of time.
One work around might be to add your function a few cells at a time. Of course, when you subsequently open the sheet again, you will probably run into the same problem.
Depending on what your function is trying to accomplish, it might be worth using the built in spreadsheet functions. There is a lot of power there. Writing a function that acts on a range of values instead of a single cell might be another, better, option. It could be triggered through a custom menu item, or by using the script manager.
Keep in mind, batch actions are your best friend when it comes to working with spreadsheets.