How to share Spreadsheet with reference to a custom (private) library - google-apps-script

I've build Spreadsheet with a lot of google script code. It started as a personal spreadsheet and script, but more people want to use my spreadsheet. I doesn't want to share the google script code to the other people, but sharing the spreadsheet is ok.
So I've created a separate Google Script project and published a version and included that version in the spreadsheet. This works great for me as owner of the spreadsheet and script.
Now I've shared the spreadsheet. That people make a custom copy of the sheet and tried to use it. When running the function in the sheet (referencing to the library):
1) getting the question to give the script the right authorisation, on base of the functions in the custom library.
2) After that Google gives a error: You do not have access to library xxx, used by your script, or it has been deleted.
So the enduser have edit right to the sheet, but the custom library is only for me. I can make this working by publishing the custom library as available for anyone with the link with EDIT rights.
Is there a way to fix this? The code in script is very important to ensure that nobody may see what's happening there.
hmmm giving al users read right, means they can access my sourcecode. Will there be no option to prevent that? Loading sourcecode form external source for example?
Another issue with giving the user read rights. I've created two custom libraries, one with the public functions and one with the private functions. The public functions library holds all the public getter and setters.
So in the sheet code there's a function:
function GetInformation()
{
LKfunctions.GetInformation();
}
LKFunctions is the public library. In the public library i've included the private library as LKfunctionsPrivate. So the function in the public library is:
function GetInformation()
{
var parsedData = LKfunctionsPrivate.getLK_JSON();
if (parsedData != "")
{
LKfunctionsPrivate.FillSettingsSheet(parsedData);
LKfunctionsPrivate.FillCastleSheet(parsedData);
}
}
When I execute the function the sheet I get the error:
"ReferenceError: 'LKfunctions' is not defined. (line 6, file 'publicFunctions.gs'
That's very strange because publicFunctions that function has the content:
LKfunctionsPrivate.getLK_JSON();
So how can the script gives me a error on LKfunctions, when using LKfunctionsPrivate. When I debug I indeed that the line is edited to LKfunctions.getLK_JSON();
Very strange.
Looks this works indead. I've merged my private and public function library to one.
One problem. I'm using a external library called Moment in my function library.
https://script.google.com/macros/library/versions/d/MHMchiX6c1bwSqGM1PZiW_PxhMjh3Sh48
When including my library, I must enable development mode on the function library to work correctly. When anyone else using my script or I disable development mode, we got the error message:
ReferenceError: "Moment" is not defined. (line 5, file "constants", project "Lords and Knights functions")

Here is an example for you to test and to check that users don't have to have edit rights on the script source.
Use this example in a spreadsheet :
//Library Project key : M_ZIuMRltotjhyMdhBDVG76tIT_UhbT9n
// choose stable version.
function test(){
testLib.searchUI();
}
it will create a small UI in your spreadsheet... the library script is viewable to anyone with the link but no one has the link

Related

Bulk update menu functions: add new function, update function without using HEAD—in MANY files at once

Google Scripts lets you create custom menus. For example:
function makeMenu(){
var ui = SpreadsheetApp.getUi();
ui.createMenu("Scripts")
.addItem("My Script","myScript")
.addToUi();
This works great if you have only ONE file you want to script.
Let's say, however, you have 50 files that you want to all have the same menu running the same functions.
You can easily update the functions themselves by putting them in a library and having those 50 files reference the library version HEAD:
function myScript(){
MyLibrary.myScript();
}
Now, if you want to update the myScript() function, you simply update it in your library, and the changes propogate.
However, I have two problems:
The only way I can see to add a NEW menu function to all 50 files is to manually go into each file and add:
function myNewScript(){
MyLibrary.myNewScript();
}
Is there any way to update them all at once?
NOTE: While you could "library out" the "makeMenu" function, and the individual scripts would then have the new menu item, they will error out when the menu function is used, as the menu function will only call the function if it exists within the file itself (as opposed to the library).
The only way I can see to use versioning with the library (so users can have a stable version while the developer works on the HEAD version) is, again, to manually go through all 50 files and update the library version.
Is there any way to use versioning without having to go into each file and manually update the library version it uses?
I did find this close-but-not-quite answer about dynamically updating menus; however, as far as I can tell, it will only work within a single script, and does not apply to libraries (see my NOTE for #1 above):
Update app script executable functions menu as new functions are added
Create menus from library itself
Call functions in library from the including script using libraryIdentifier.libraryFunction
Library file.gs:
function mkMenu(libraryIdentifier="MyLibrary"){
SpreadsheetApp.getActive()
.addMenu('ClickMe!',[{name:'alert!',functionName:`${libraryIdentifier}.alertLib`}])
}
function alertLib() {
SpreadsheetApp.getUi().alert('Library function running!');
}
Including script.gs:
function onOpen(){
//You need to pass a specific library identifier, if it is different from `MyLibrary`
MyLibrary.mkMenu();
};
Alternatively, Use google-apps-script-api to modify multiple files' scripts, which can also be used to modify the currently available library version in dependencies:{version:versionNumber,userSymbol:libraryIdentifier} in appsscript.json
I actually found a simpler way to do this based on TheMaster's answer (thank you!).
I had not thought to simply pass the library reference to Menu.addItem as part of the function name.
So within the library you have something like:
function makeMenu(){
var ui = SpreadsheetApp.getUi();
ui.createMenu("Scripts")
.addItem("My New Script","Library.myNewScript")
.addToUi();
}
function myNewScript(){
SpreadsheetApp.getUi().alert("Hello world!");
}
And within the file that references the library all you need is:
function onOpen(){
Library.makeMenu();
}
This removes the need to list function myNewScript within the file's script.
Maybe there is something about TheMaster's answer that makes it more modular that I am not understanding, but based on my current understanding, what I have just described is the simplest way to accomplish what I want.
Thanks again!

Problem loading Google App Scripts Library - get unknown function error

I'm trying to add libraries to my google sheets script. I can add them, but trying to use them in my sheets, I get an "unknown function" error.
For example, in my project's app script, I've added LodashGS v6. I can click on the library options and select 'open in a new tab' and can see the library's functions, in this case, LodashGS has a function called load, which loads the full lodash library. When I use LodashGS in the sheets, I get the "unknown function" error.
I've added const _ = LodashGS.load(); into the Code.gs file, which should give me access to the underscore in my sheets. But using the _ will give the the same error.
How can I access these libraries. I feel like I'm missing a minor detail but can't see to find it.
Here's a link to the google sheet I'm using - https://docs.google.com/spreadsheets/d/1vCkvohhecgSNEt5ybznDIRyzT70lDLz3TWxU-i2VwgQ/edit?usp=sharing
Issue:
You cannot use Apps Script methods (including from imported libraries) directly as custom functions. You have to define a function in Apps Script in order to use it as a custom function.
Also, you can access functions, not variables. For example, when doing const _ = LodashGS.load();, _ is a variable (actually a constant! But the point remains) and cannot be used as a custom function. Hence, _.now() is not recognized.
Custom function are actually strict with the function syntax used. As you can see in the naming guidelines:
The name of a custom function must be declared with the syntax function myFunction(), not var myFunction = new Function().
(Arrow function syntax apparently is also allowed, so this documentation could arguably be updated, but the main point remains).
Solution:
Wrap your methods inside a function if you want to use it as a custom function. You can either use the function keyword, as you already did for getTime(), or arrow functions, for example:
const getTime = () => LodashGS.load().now();
Note:
Please take into account that the name of a custom function cannot end with an underscore (_) (see the previously referenced naming guidelines).

How do I call CreateMenu() from within an Apps Script Library?

I'm building a library within Apps Script and would like to make it pretty self sustaining. Essentially, if someone wants to use this Lib in their Google Sheet, they'd need simply to put in the following code in their Sheets Script
function onOpen(){
myLib.initialize();
}
And this would set up the sheet as needed. One of the things I do here is to actually add a menu item in the Sheet itself. Therefore, in my Library code, I'll have something like;
function initialize(){
var ui = SpreadsheetApp.getUi();
ui.createMenu('Custom Menu')
.addItem('First item', 'someCallback')
.addToUi();
}
The problem with this approach is that when the menu item is clicked, the "someCallback" function cannot be found. This makes sense since it resides in a separate library. It should be called using dot notation like this: myLib.someCallback
Is there some way that I can get the Library Identifier at run time so that I can create the string required to create the menu?
I can do a few things which are not my first choices such as:
pass the "myLib" string to the initialize function
type in the "myLib" to the addItem() function directly, but I"m thinking this is not good practice, what if the end user changes the identifier from myLib to something else.
Maybe there's some Runtime way that I can get the Library Identifier?
Answer:
Unfortunately at the moment this isn't possible to do.
More Information:
I tested out a few things using both the this keyword and the eval() function in order to either dynamically get the redefined identifier of myLib or calling someCallback directly in the initialize function, however even embedding the callback function inside initialize:
funciton initialize() {
var cb = function someCallback() {
// do stuff
};
var ui = SpreadsheetApp.getUi();
ui.createMenu('Custom Menu')
.addItem('First item', 'cb')
.addToUi();
or:
function initialize() {
function someCallback() {
//do stuff
}
var ui = SpreadsheetApp.getUi();
ui.createMenu('Custom Menu')
.addItem('First item', 'someCallback')
.addToUi();
}
throw the same error with either cb or someCallback not defined.
Problems with this:
To answer your direct question
Maybe there's some Runtime way that I can get the Library Identifier?
The answer is yes. Kind of.
The this keyword is pretty powerful, and at runtime you can use it to get the name of the user-set identifier in the following way:
function onOpen(){
myLib.initialize(this);
}
And in myLib.initialize():
function initialize(name) {
console.log(Object.keys(name));
}
The console showing: ['myLib', 'onOpen']
This issue is, is that while the names of imported libraries come before the list of functions defined in the user's script, if more than one library has been imported you have no way of telling which of the keys refers to your library, and which refer to the others. Resultantly just calling name[0] won't cut it.
Doing Some Digging:
I took to Google's Issue Tracker and discovered that there are some feature requests which have very similar use cases to yours:
AddMenu should be able to use anonymous functions or at least pass parameters
AddMenu should be able associate an object method to a menu entry, not just a global function
While the use-cases aren't exactly the same, the underlying issue on both is that anonymous functions nor parameters can be passed to libraries that use the UI methods of Apps Script.
There is also this related feature request which asks to implement a catch-all style method which runs a method if a library method which doesn't exist is called:
_noSuchMethod_ for Library ( undefined method in google apps script )
What to do:
Normally, I would suggest following the Issue Tracker links and clicking the star. In this case, however, as your use case isn't exactly defined in the scope of the aforelinked feature requests, I would urge you to use this link to file an Apps Script feature request and detail all the information you put here in your question.
As far as workarounds go; the best thing you can do in the meantime is to document that the initialize() function needs to be passed the identifying name of the library. It's not an elegant solution by any standard, but given the complexity of this and the feature requests that are yet to be realised this may be the easiest option.
It might be possible to loop through the Object.keys(this) array but I feel like this is a very hacky workaround which could introduce more problems than necessary (especially if, for example, another library creator also happens to have a function called someCallback...)
References:
The JavaScript this keyword - w3schools
Google's Issue Tracker

Syntax Error on internal function when calling another function in a library

I am having an issue with a script that references a library. My issue is not connecting to the library, its that my script calls a function, but when it runs, it errors out on a function that I am not referencing.
In sheet A, I have this code:
function DataPull() {
MasterScriptLibrary.DataPullBillable()
}
In MasterScriptLibrary (project's full name is 2020 Master Script Library), I have 2 distinct functions (they don't reference each other at all) called convertPersonnel and DataPullBillable.
When the above function runs from sheet A, I get an error message:
Syntax error. (line 66, file "convertPersonnel", project "2020 Master Script Library")
Line 66 of the library:
var data = activeRange.filter(element => element.join("") != "");
Any idea what could be happening here?
Problem
When a library is required in Google Apps Script project, it is run in the environment of the calling script. Thus, if any of the language features / syntax used by the library are not supported by the runtime of the calling script, it will throw a corresponding error.
Runtimes
One of the most common issues since GAS switched from Rhino to V8 runtime is caused by trying to use ES6 features (some were supported even in the older one, but the comprehensive list is gone from the developers resource since the overhaul).
Syntax Error
You are trying to use syntax not supported in the older runtime, hence the SyntaxError. The message points directly to the issue (line 66) but does not provide info on what is wrong, so the confusion is understandable (the fact that it is valid ES6 syntax adds to it).
The actual problem is the presence of an arrow function.
Check for runtime
To check which runtime is used (apart from the plaque at the top if using script editor), open manifest file (appsscript.json) and find the runtimeVersion field, it will be set to either "V8" or "DEPRECATED_ES5" (Rhino).
Newer scripts are guaranteed to use V8, but you have to migrate older ones manually by either setting the abovementioned field to V8 (you can also "downgrade" as needed) or selecting the option in "Run" menu.

wrapping UiApp functions in a Library

I am using sort of wrapping functions to add widgets to UiApp, like the sample below
function newButton(text, handler, callBack) {
var app = UiApp.getActiveApplication();
return app.createButton(text).addClickHandler(app.createServerClickHandler(handler).addCallbackElement(callBack));
}
it works fine when used within the same script like below
panel.add(newButton("Submit", "myHandler", panel));
but when including newButton function within a Library (ex. myLib) and it's called as myLib.newButton, I would get an error:
"Error encountered: Unknown macro myLib.myHandler"
panel.add(myLib.newButton("Submit", "myHandler", panel));
Any way of avoiding this problem, while keeping myHandler in the current script (not in myLib)?
Thanks, Fausto
Yes it can.
In your current script (wherein you imported the reference to the library)
create;
function myLib.myHandler(){
}
As your handler (string) keep passing the name 'myHandler'. The above function will be called by the button created in your library script.
It seems like apps script is using this namespace structure to avoid name collision.
You should keep in mind that this could purposely be not-documented by google for a reason. Maybe this current structure could be changed in the future. Or maybe im just being paranoid ;-)