I want to see if there is a way to obtain a list of all the functions I have in a Google Apps Script project. I've seen multiple threads on getting a list of all of your Google Apps Script projects but none as of yet for listing all of the functions in each project. Does anyone know if this is possible? I've looked through the Google Apps Script Reference Overview but I wasn't able to find anything that stood out to me (I, of course, could've missed it). If anyone has any suggestions, please let me know!.
The best example I can provide is:
I have a Google Spreadsheet file. Attached to that Google Spreadsheet is a GAS project (accessed through the Google Sheet menu "Tools -> Script Editor") that has a couple of different functions used to grab values from the sheet, do some calculations and post the results to a different sheet.
What I am trying to accomplish: Run some sort of function that can provide me a list of all of the functions I have in the GAS project (preferably as string values). Example would be:
["runMyCalculations","myOnEdit","sortClosedFiles","formatSheets"]
All of these are functions that can only be run if I open up the Script Editor and select it in the drop-down menu and click the "Run" button.
What I want to be able to do is create a dynamic list of all the functions I have so I can pass them into an "on open" triggered function that creates a custom menu in the sheet, listing out all of the functions I have. I want this so I can simply make changes to my sheet, go to the drop-down menu and run the function I need to run, rather than having to open up the Script Editor.
You can use the Apps Script API to get all the content out of an Apps Script file.
The following code has the option of passing in a file name to get. You must supply the Apps Script file ID. Passing in a gs file name is optional. Provided are 3 functions. The function that does all the work, a function to call that function with the parameters for testing, and a logging function. An OAuth library is not needed because the token is acquired from the ScriptApp service.
NOTE: You will need to enable the Apps Script API, and approve permission to your Drive in order for this code to work. Make sure to check the return from the UrlFetchApp.fetch() call the first time that you run this code for an error message. It may have a link that you need to use to enable the Apps Script API.
function getFuncNames(po) {
var allFiles,dataContentAsString,downloadUrl,fileContents,fileData,i,options,
theAccessTkn,thisFileName;
var ndxOfFunction=0,counter=0, ndxOfEnd=0, functionName="", allFncNames=[],
hasSpaces = 0;
var innerObj, thisFile, fileType = "", thisGS_Content,howManyFiles, allGsContent="";
/*
Get all script function names. If no gs file name is provided, the code
gets all the function names.
*/
/*
po.fileID - required - The Apps Script file ID
po.gsFileName - optional - the gs code file name to get - gets just one
file instead of all files
*/
//ll('po',po);
if (!po.fileID) {
return false;
}
theAccessTkn = ScriptApp.getOAuthToken();//Get an access token for OAuth
downloadUrl = "https://script.google.com/feeds/download/export?id=" +
po.fileID + "&format=json";//create url
options = {
"kind": "drive#file",
"id": po.fileID,
"downloadUrl": downloadUrl,
"headers": {
'Authorization': 'Bearer ' + theAccessTkn,
},
"contentType": "application/vnd.google-apps.script+json",
"method" : "GET"
};
fileData = UrlFetchApp.fetch(downloadUrl, options);//Get all the content from the Apps Script file
//ll('fileData',fileData)
dataContentAsString = fileData.getContentText();
fileContents = JSON.parse(dataContentAsString);//Parse string into object
allFiles = fileContents.files;//All the files in the Apps Script project
howManyFiles = allFiles.length;
for (i=0;i<howManyFiles;i++) {
thisFile = allFiles[i];//Get one inner element that represents one file
if (!thisFile) {continue;}
fileType = thisFile.type;
if (fileType !== "server_js") {continue;}//This is not a gs file - its HTML or json
thisFileName = thisFile.name;
//ll('typeof thisFileName',typeof thisFileName)
//ll('thisFileName',thisFileName)
//ll('equal',po.gsFileName !== thisFile.name)
if (po.gsFileName) {//Is there a setting for the file name to restrict the search to
if (po.gsFileName !== thisFile.name) {//The name to search for is not this file name
continue;
}
}
thisGS_Content = thisFile.source;//source is the key name for the file content
allGsContent = allGsContent + thisGS_Content;
}
//ll('allGsContent',allGsContent)
while (ndxOfFunction !== -1 || counter < 1000) {
ndxOfFunction = allGsContent.indexOf("function ");
//ll('ndxOfFunction',ndxOfFunction)
if (ndxOfFunction === -1) {break};
allGsContent = allGsContent.slice(ndxOfFunction+9);//Remove everything in front of 'function' first
ndxOfEnd = allGsContent.indexOf("(");
functionName = allGsContent.slice(0,ndxOfEnd);
allGsContent = allGsContent.slice(ndxOfEnd+2);//Remove the
hasSpaces = functionName.indexOf(" ");
if (hasSpaces !== -1) {continue;}
if (functionName.length < 150) {
allFncNames.push(functionName);
}//Any string over 150 long is probably not a function name
counter ++;
};
//ll('allFncNames',allFncNames)
return allFncNames;
};
function runOtherFnk() {
getFuncNames({fileID:"Your File ID here",gsFileName:"Code"});
}
function ll(a,b) {
//Logger.log(typeof a)
if (typeof b === 'object') {
b = JSON.stringify(b);
}
Logger.log(a + ":" + b)
}
The following code extracts file names from the this object:
function getAllFnks() {
var allFnks,fnkStr,k;
allFnks = [];
for (k in this) {
//Logger.log(k)
//Logger.log(typeof k)
//Logger.log(this[k])
//Logger.log(typeof this[k])
fnkStr = this[k];
if (fnkStr) {
fnkStr = fnkStr.toString();
//Logger.log(typeof fnkStr)
} else {
continue;
}
//Logger.log(fnkStr.toString().indexOf('function'))
if (fnkStr.indexOf('function') === 1) {
allFnks.push(k);
}
}
Logger.log(allFnks)
Logger.log('Number of functions: ' + allFnks.length)
}
Related
I have created a Google apps script attached to a google sheet (where I have various methods manipulating the spreadsheet), and I have deployed it as API executable (enabling OAuth etc). Target is to call those methods via REST from an external location not part of Google cloud (like an independent React client, or a standalone server, or my local machine)
Question is: How can I call this from a standalone javascript (like a node.js script executed on my local machine? I do have the script URL (script id) , the secret and the key, but don;t know how to use them all.
Could you help with some sample code, pointers, etc. It looks like my google searches hit only unrelated topics...
You can check this example on how to call the script as an API executable. You will see that the way to call the script from different languages is similar for example using JavaScript, you need to also take note on some important information like:
The basic types in Apps Script are similar to the basic types in JavaScript: strings, arrays, objects, numbers and booleans. The Execution API can only take and return values corresponding to these basic types -- more complex Apps Script objects (like a Document or Sheet) cannot be passed by the API.
An example to make a call the way that you currently want using Apps script would be like:
Target Script
/** This is the Apps Script method these API examples will be calling.
*
* It requires the following scope list, which must be used when authorizing
* the API:
* https://www.googleapis.com/auth/spreadsheets
*/
/**
* Return a list of sheet names in the Spreadsheet with the given ID.
* #param {String} a Spreadsheet ID.
* #return {Array} A list of sheet names.
*/
function getSheetNames(sheetId) {
var ss = SpreadsheetApp.openById(sheetId);
var sheets = ss.getSheets();
return sheets.map(function(sheet) {
return sheet.getName();
});
}
This is the script that you have setup as an API executable and you can call this script using JavaScript like this:
// ID of the script to call. Acquire this from the Apps Script editor,
// under Publish > Deploy as API executable.
var scriptId = "<ENTER_YOUR_SCRIPT_ID_HERE>";
// Initialize parameters for function call.
var sheetId = "<ENTER_ID_OF_SPREADSHEET_TO_EXAMINE_HERE>";
// Create execution request.
var request = {
'function': 'getSheetNames',
'parameters': [sheetId],
'devMode': true // Optional.
};
// Make the request.
var op = gapi.client.request({
'root': 'https://script.googleapis.com',
'path': 'v1/scripts/' + scriptId + ':run',
'method': 'POST',
'body': request
});
// Log the results of the request.
op.execute(function(resp) {
if (resp.error && resp.error.status) {
// The API encountered a problem before the script started executing.
console.log('Error calling API: ' + JSON.stringify(resp, null, 2));
} else if (resp.error) {
// The API executed, but the script returned an error.
var error = resp.error.details[0];
console.log('Script error! Message: ' + error.errorMessage);
} else {
// Here, the function returns an array of strings.
var sheetNames = resp.response.result;
console.log('Sheet names in spreadsheet:');
sheetNames.forEach(function(name){
console.log(name);
});
}
});
Please note as well that there are some limitations that you may want to check before further perform tests.
I am trying to call the (new) alpha GA Admin API for the simple task of listing all the Accounts that I have access to... I am at a stage where I call the API but I do not get any error message nor I see the information on the google sheet. Can you please help?
function listGA4Accounts() {
var sheet = _setupListGA4AccountsSheet();
var accounts = AnalyticsAdmin.Accounts.list();
if (accounts.items && accounts.items.length) {
for (var i = 0; i < accounts.items.length; i++) {
var account = accounts.items[i];
var rowNum = i+2;
sheet.getRange("A" + rowNum).setNumberFormat('#')
.setValue(account.name).setBackground(AUTO_POP_CELL_COLOR);
sheet.getRange("B" + rowNum)
.setValue(account.displayName).setBackground(AUTO_POP_CELL_COLOR);
sheet.getRange("C" + rowNum)
.setValue(account.createTime).setBackground(AUTO_POP_CELL_COLOR);
}
}
}
The above was adapted from the old code being used for Universal Analytics/GA3 and used to work just fine. What I am missing? I also have a standard GCP project in place and the API is enabled for that GCP project.
Any help/thoughts on the above are highly appreciated.
Thanks.
You have been quite close to the solution.
TIPS for debugging the API response:
prompt the API response in the console with Logger.log(JSON.stringify(<API RESPONSE>))
copy the log and paste it on a JSON formatter website like this one: https://jsonformatter.curiousconcept.com/
check the actual structure
OPTIONAL: copy the formatted JSON from the page & save it in the script as a variable and use this instead of the API response to prepare the code. Once it's working properly with the saved data you can switch back to the data from the API request.
Following things that I changed:
removed var sheet = _setupListGA4AccountsSheet(); (was not relevant for testing the API response)
just changed the way the JSON object is accessed bc it's a nested one, to get an account item it's necessary to write <variable name>.accounts.item
Here is the code that can be copied as-is to App Script editor and can be tested:
function listGA4Accounts() {
var accounts = AnalyticsAdmin.Accounts.list();
if (accounts && !accounts.error) {
accounts = accounts.accounts; // <== this is why it didn't work is a nested JSON
Logger.log(accounts[0]);
for (var i = 0, account; account = accounts[i]; i++) {
Logger.log(account);
/**
* PLACE your code here
*/
}
}
}
new to Google Scripts and I've looked through other posts on Stack Overflow as well but couldn't find a good answer.
I'm using data collected in Google Sheets to search for a file in Google Drive and transfer ownership of the file. I have google form that my users fill out, once submitted using an add-on I create a file based on the data that was submitted on the form. Now with the script, I'm trying to go gather certain information from sheets such as name, email, and company name -
Sample data image here.
What I have thus far:
function myFunction() {
//Get google sheets
var spreadsheetId = '1WvIIoYdmuIB5BQ3KgSYOOIiEn-K_GTzCkb7rITzRFck';
//get certain values from sheets
var rangeName = 'MDP Form!C25:E';
var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values;
if (!values) {
Logger.log('No data found.');
} else {
Logger.log('Name, Email, Customer:');
for (var row = 0; row < values.length; row++) {
// Print columns C and E, which correspond to indices 0 and 4.
Logger.log('Name: %s, Email: %s, Company: %s', values[row][0], values[row][1], values[row][2]);
//Utilities.sleep(90000);
//Searching through google drive
var name = (values[row][0]);
var email = (values[row][1]);
Logger.log(email);
var company = (values[row][2]);
var fileName = ('Mutual Delivery Plan ' + company + ' - ' + name);
Logger.log(fileName);
//add a 1 minute delay
//Utilities.sleep(90000);
//search for target folder
var folder = DriveApp.getFolderById('1whvRupu9hWdyl2CqSF-KvdVj8VE6iiQu');
//search for file by name within folder
var mdpFile = folder.searchFiles(fileName);
//transfer ownership
mdpFile.setOwner(email);
}
}
}
Problem:
The script works for the most part except for the last line "setOwner" is not a function. I've tried creating a separate function for this, used some other suggestions on other posts but still cannot get this to work. If anyone has ideas around what might I be missing here or suggestions that would be super helpful. Thanks!
I believe your goal as follows.
You want to transfer the owner of the file when the file with fileName is found in folder.
For this, how about this answer?
Modification points:
Although you say The script works for the most part except for the last line "setOwner" is not a function., if your script in your question is the current script, how about the following modification?
In your script, fileName is 'Mutual Delivery Plan ' + company + ' - ' + name, and fileName is used with var mdpFile = folder.searchFiles(fileName);. In this case, an error occurs. Because params of searchFiles(params) is required to be the query string.
I think that in your case, it's "title='" + fileName + "'".
Also searchFiles(fileName) returns FileIterator. This has already mentioned by the existing answer. Because at Google Drive, the same filenames can be existing in the same folder and each files are managed by the unique ID. So here, it is required to be modified as follows.
I think that in your case, the following flow is useful.
Confirm whether the file is existing using hasNext().
When the file is existing and the owner is you, the owner of the file is changed to email.
When above points are reflected to your script, please modify as follows.
Modified script:
From:
var mdpFile = folder.searchFiles(fileName);
//transfer ownership
mdpFile.setOwner(email);
To:
var mdpFile = folder.searchFiles("title='" + fileName + "'");
while (mdpFile.hasNext()) {
var file = mdpFile.next();
if (file.getOwner().getEmail() == Session.getActiveUser().getEmail()) {
file.setOwner(email);
}
}
If you don't need to check whether the owner is you, please remove if (file.getOwner().getEmail() == Session.getActiveUser().getEmail()) {.
Note:
In this case, when the file with the filename of fileName is not existing in folder, the script in the if statement is not run. Please be careful this.
Also, when there are several files with the same filename in folder, the owner of those is changed to email.
References:
searchFiles(params)
FileIterator
hasNext()
next()
getActiveUser()
Folder.searchFiles() returns a fileIterator not a file. If it's the only file with that name then you can usually getaway with mdpFile.next();
File Iterator
As part of a larger Google App Script webapp, I want to create a rudimentary file system with files/folders in the user's Google Drive. I'm doing this through a element where each would be a different folder (prefixed with a '*') or file.
I have setup the webapp HTML to include the element, but within this element I call a script that will populate the via a call to google.script.run.withSuccessHandler. It appears that this code runs as I'd expect, but the result of DriveApp.getRootFolder() is null, thereby making me unable to access the file structure.
// In the HTML file.
...
<head>
<script>
...
// Populate options in the file/folder list based on the provided folder.
function setFiles(folder)
{
alert(folder);
return;
/* // Get the select item.
var e = document.getElementById("file-select");
// First list all the folders at the top.
//#TODO Adding an asterick on folders to identify them for now, maybe have a different method later?
var folderI = folder.getFolders();
var i = 0;
while(folderI.hasNext())
{
var fldr = folderI.next();
e.innerHTML += "<option id='f_'" + i + "'>*" + fldr.getName() + "</option>";
i++;
}
// Now list all the files in the current directory.
i = 0;
var fileI = folder.getFiles();
while(fileI.hasNext())
{
var fle = fileI.next();
e.inner.HTML += "<option id='f_'" + i + "'>*" + fle.getName() + "</option>";
i++
}
*/
....
</script>
</head>
<body>
...
<div id="select-files">
<select id="file-select" size="10">
<script>
// Populate the initial file/folder list.
google.script.run.withSuccessHandler(setFiles).getRootFolder();
</script>
</select>
</div>
...
// In code.gs
/**
* Returns the root folder for the user.
* #return The root folder of the user.
*/
function getRootFolder()
{
return DriveApp.getRootFolder();
}
This is the code as I'm testing it now, hence my commenting out most of setFiles(). alert() results in 'null', but I'd expect it to be an 'Object [Object]' type that I could iterate through.
Interestingly, when I've added Logger.log() lines in the code.gs file, no log output is produced (I can't figure out why, because if I change the return value of getRootFolder() to a string, that string is displayed in the alert, so I know the code is entering that function correctly.
I'm wondering if this is a misunderstanding such that Google Drive (or maybe, generally, Google App Script specific objects) cannot be passed to an HTML file, though I couldn't find any clear documentation that this is the case.
As Cooper said in the comments, the Folder type is not legal to send to the client. If you look at what a Folder contains, it is purely functions, which are not allowed to be sent over.
All that client-side you commented out in setFiles cannot function in the user's browser. Even if you were able to pass the Folder code into the client, what would folder.getFolders() mean to the user's browser? It would start looking for the rest of the code from DriveApp, which doesn't exist in the browser, and still fail.
I'm wondering if this is a misunderstanding such that Google Drive (or maybe, generally, Google App Script specific objects) cannot be passed to an HTML file
What you get passed to the HTML file is documented here. Pay special attention to how google.script.run works.
No, you cannot pass the entire environment of your server-side code to the client (e.g. pass all of DriveApp and its dependences over to the client).
What you can do on both sides is construct your own version of Folder which exports the strings on the server side and reconstructs them on the client side. Note that arrays of strings are OK, so I would put things like the child, parent folder names and IDs in arrays. Just to be safe, I use JSON stringify/parse to strip functions out. This example works without the JSON part, but on more complicated objects it can be nice to clean them up.
client-side code
// just to log that it works
google.script.run.withSuccessHandler(response => {
response = JSON.parse(response);
console.log({response})
}).getFolder();
Code.gs
// client-code calls this to get folder info
function getFolder(id) {
return JSON.stringify(new Folder_(id ? DriveApp.getFolderById(id) : DriveApp.getRootFolder()));
}
// constructor for a `folder` suitable to send to the client
function Folder_(folder) {
this.id = folder.getId();
this.name = folder.getName();
this.foldersIds = [];
this.foldersNames = [];
this.parentsIds = [];
this.parentsNames = [];
this._extractFolders(folder, "folders");
this._extractFolders(folder, "parents");
}
// one function for both "getFolders" and "getParents"
Folder_.prototype._extractFolders = function(folder, type) {
var folders = folder["get" + type.replace(/^./, function(str){return str.toUpperCase()})]();
while (folders.hasNext()) {
var folder = folders.next();
this[type + "Ids"].push(folder.getId());
this[type + "Names"].push(folder.getName());
}
};
Whenever I attempt to display a UI dialog (e.g. msgBox or alert) it works fine when invoked via a menu item (e.g. from Google Sheets), but it hangs my script if I try to invoke it from the Google Apps Script editor (e.g. via Run > Run function).
My guess is it's because the Google Apps Script editor can't display any UI. To resolve this, I'd like to create a wrapper function that checks how the script was run, and not present UI depending on the source.
The "Executions" screen has the notion of Type (Editor, Standalone, Trigger):
This makes me think there is a way to get this type in code somehow.
Psuedo code of what the function might look like:
function showMessage(message) {
var scriptSource = ???;
if (scriptSource === "Standalone") {
Browser.msgBox(message);
} else {
console.log(message);
}
}
How would I get the scriptSource?
The closest thing I can find is TriggerSource, but that is missing the enum values 'Editor' and 'Trigger'. Furthermore, it's a property only available on a Trigger. I don't know how to access the current trigger. From my understanding, that's only available via the event object (e.g. via triggerUid) on functions acting as triggers. This method I'm running in the apps script editor doesn't have access to an event object.
Not the best solution, but my current workaround is to create 3 versions of each function, and append how it was invoked to the name.
For example, if there was a "Hello World" function:
function onOpen() {
var menu = [
{name: 'Hello World', functionName: 'helloWorldViaMenu_'},
];
SpreadsheetApp.getActive().addMenu('Custom', menu);
}
function helloWorldViaMenu_() {
helloWorld_(false);
}
function helloWorldViaEditor() {
helloWorld_(true);
}
function helloWorld_(invokedFromEditor) {
if (invokedFromEditor) {
Logger.log("Hello world");
} else {
Browser.msgBox("Hello world");
}
}
helloWorldViaEditor is the only that doesn't have a _ at the end so it can be selected via the "Select function" Editor UI dropdown.
You want to know whether the current project is the container-bound script type or the standalone script type.
You want to use Browser.msgBox().
I could understand about your question as above. In order to achieve it, as a workaround,I would like to propose to use Apps Script API. The flow of sample script is as follows. I think that there are several workarounds for your situation. So please think of this as one of them.
Retrieve the parent ID of the project using the method of projects.get in Apps Script API. The parent ID means that the file ID of Google Docs.
When the parent ID is returned, it is found that the project is the container-bound script type.
When the parent ID is NOT returned, it is found that the project is the standalone script type.
When the mimeType of parent ID is Google Form, Browser.msgBox() cannot be used. So the if statement is used for this.
Sample script:
This is a sample script. In this sample script, the script ID of current project is used. Of course, you can also manually give the script ID.
var id = ScriptApp.getScriptId(); // Retrieve scriptId of current project.
var url = "https://script.googleapis.com/v1/projects/" + id + "?fields=parentId";
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}});
res = JSON.parse(res.getContentText());
if ("parentId" in res) {
Logger.log("Container-bound script type.")
var mimeType = DriveApp.getFileById(res.parentId).getMimeType();
if (mimeType === MimeType.GOOGLE_FORMS) {
Logger.log("Browser.msgBox() cannot be used at Google Form.");
} else {
Browser.msgBox("Hello world");
}
} else {
Logger.log("Standalone script type.")
Logger.log("Hello world");
}
Note:
When you use this script, please do the following flow.
Enable Apps Script API at API console.
At least, add the following scopes to the manifests.
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/script.projects.readonly
If in your script, other scopes are required to be added, please add them. And if you want to use the automatically installer of scopes with the script editor, you can achieve it using a library. You can see the detail information at here.
References:
Apps Script API
Manifests
projects.get
Taking Advantage of Manifests by GAS Library
If I misunderstand your question, I'm sorry.
Edit:
You want to confirm whether the function is called from the script editor or the custom menu.
If my understanding is correct, how about this sample script? This is a sample script. The process list can be retrieved by giving the script ID and function name. In this sample script, using "ProcessType" of processes.listScriptProcesses in Apps Script API, it confirms whether the function is called from the script editor or the custom menu.
Sample script:
This is a sample script. The process list can be retrieved by giving the script ID and function name.
When you use this script, please enable Apps Script API at API console, and add a scope of https://www.googleapis.com/auth/script.processes to the manifests.
The how to use this script is as follows.
Run addCustomMenu().
Run sampleFunction at the custom menu.
By this, Call from custom menu is shown in log.
Run sampleFunction at the script editor.
By this, Call from script editor is shown in log.
Script:
function addCustomMenu() {
SpreadsheetApp.getUi().createMenu('sampleCustomMenu').addItem('sample', 'sampleFunction').addToUi();
}
function sampleFunction() {
var scriptId = ScriptApp.getScriptId();
var functionName = "sampleFunction";
var url = "https://script.googleapis.com/v1/processes:listScriptProcesses?scriptId=" + scriptId + "&scriptProcessFilter.functionName=" + functionName;
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}, muteHttpExceptions: true});
res = JSON.parse(res);
if (!("processType" in res.processes[0])) {
Logger.log("Call from custom menu")
} else if (res.processes[0].processType == "EDITOR") {
Logger.log("Call from script editor")
}
}
References:
Apps Script API
Manifests
processes.listScriptProcesses
ProcessType
Making Dialogs
You can run them from the menu or the script editor. They work the same.
function makeAmenu(){
SpreadsheetApp.getUi().createMenu('A Menu')
.addItem('Run my Dialogs', 'showMyDialogs')
.addToUi();
}
function showMyDialogs(){
var ui=SpreadsheetApp.getUi();
ui.alert('This is an alert');
ui.prompt('This is a prompt');
var html=HtmlService.createHtmlOutput('<p>This is a modeless dialog</p><input type="button" value="Close" onClick="google.script.host.close();" />');
ui.showModelessDialog(html, 'Dialog');
}
If you run a script from here:
The you have to go here to see it: