Until Google extends the import/export API to container-bound Apps Script projects, I have moved most of my project to a library which can use that API, and then made Google Docs project into a shell that just calls through to the library.
My problem is having the library access the same properties (PropertiesService
) as the Google Doc project. Since I have existing users of my Docs Add-on, I need to keep using these properties.
In my Google Doc project, I tried
$.PropertiesService = PropertiesService;
(where $
is my library).
It didn't work. The library kept using its own properties.
So then I tried:
function _mock(obj) {
var ret = {};
for(var key in obj) {
if(typeof obj[key] == 'function') {
ret[key] = obj[key].bind(obj);
} else {
ret[key] = obj[key];
}
}
return ret;
}
$.PropertiesService = _mock(PropertiesService);
Still not working. Trying again:
function _mock(obj) {
var ret = {};
for(var key in obj) {
if(typeof obj[key] == 'function') {
ret[key] = (function(val) {
return function() {
return val.apply(obj, arguments);
};
})(obj[key]);
} else {
ret[key] = obj[key];
}
}
return ret;
}
$.PropertiesService = _mock(PropertiesService);
This works.
At this point, I'm wondering:
Why did the first two ways not work, but the third way did?
Can I expect this to continue working?
Is there a better way to have a library access the main script's properties?
Documentation is sparse. There is this, but the PropertiesService
is not mentioned.
Sharing of resources
As you are aware, libraries have shared and non-shared resources. PropertiesService
is listed under non-shared resources, meaning that the library has its own instance of the service that is accessed when you reference it in the library code.
const getStore = () => PropertiesService.getScriptProperties();
If the function above is declared in the library, it will use the library's resource, if in the calling script - its own instance.
V8 runtime solution
V8 runtime does not create a special context for your code and gives you access to built-in services directly. Because of this when using the runtime, the service can be injected by simply defining or replacing a property on a global this
:
//in the library;
var getProperty = ((ctxt) => (key) => {
var service = ctxt.injectedService;
var store = service.getScriptProperties();
return store.getProperty(key);
})(this);
var setProperty = ((ctxt) => (key, val) => {
var service = ctxt.injectedService;
var store = service.getScriptProperties();
return store.setProperty(key, val);
})(this);
var inject = ((ctxt) => (service) => ctxt.injectedService = service)(this);
var greet = ((ctxt) => () => {
var store = ctxt.injectedService.getScriptProperties();
return store.getProperty("greeting") || "Ola!";
})(this);
//in the calling script;
function testSharedResources() {
PropertiesService.getScriptProperties().setProperty("greeting", "Hello, lib!");
$.inject(PropertiesService);
Logger.log($.greet()); //Hello, lib!
$.setProperty("greeting", "Hello, world!");
Logger.log($.greet()); //Hello, world!
}
In some contexts global this
will be undefined
(I encountered this when adding a library to a bound script). In this case, simply define a private global namespace (to avoid leaking to the caller script):
//in the library;
var Dependencies_ = {
properties : PropertiesService
};
var use = (service) => {
if ("getScriptProperties" in service) {
Dependencies_.properties = service;
}
};
//in the calling script;
$.use(PropertiesService);
Rhino runtime solution
Older Rhino runtime, on the other hand, creates a special implicit context. This means that you have no access to built-in services or the global this
. Your only option is to bypass calling the service in the library (your approach #3 is perfect for doing so).
Questions
All issues with your approaches boil down to:
But there is a catch: all 3 approaches do work as designed.
First, Approach one does work if you specifically reference the PropertiesService
on $
. This makes sense as the library is included as a namespace with members mapped to global declarations in the library. For example:
//in the caller script
PropertiesService.getScriptProperties().setProperty("test", "test");
$.PropertiesService = PropertiesService;
Logger.log( $.PropertiesService.getScriptProperties().getProperty("test") ); // "test"
Logger.log( $.getProperty("test") ); // "null"
//in the library
function getProperty(key) {
var store = PropertiesService.getScriptProperties();
return store.getProperty(key);
}
Approach two also works. Binding of the function in the caller script does not change the fact if called in the library it receives library context, but if you call the bound copy directly in the calling script, it works:
//in the caller script
PropertiesService.getScriptProperties().setProperty("test", "test");
var bound = $.PropertiesService.getScriptProperties.bind(PropertiesService);
var obj = { getScriptProperties : bound };
$.PropertiesService = obj;
Logger.log( bound().getProperty("test") ); // "test"
Logger.log( $.getProperty("test") ); // "null"
Now, why does the third approach work out of the box? Because of the closure resulting from the wrapped function capturing the PropertiesService
of the calling script and applying the getScriptProperties
method. To illustrate:
//in the caller script
var appl = {
getScriptProperties : (function(val) {
return function() {
return val.apply(PropertiesService);
};
})(PropertiesService.getScriptProperties)
};
$.PropertiesService = appl;
Logger.log( $.getProperty("test") ); // "test"
Yes and no. Yes, because your _mock
function behavior exhibits the expected behavior in all cases. No, because apply
relies on the getScriptProperties
not being implemented as an arrow function where this
override will be ignored.
For Rhino runtime - don't think so. For V8 - direct injection of the service will suffice.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With