@mcpher/gas-fakes 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -873
- package/gasmess/bruce/basic.js +24 -0
- package/gasmess/bruce/docelems.js +50 -0
- package/gasmess/bruce/gasfakes.json +8 -0
- package/gasmess/bruce/pbx.js +138 -0
- package/gasmess/tanaike/for_simple_test.js +6 -0
- package/gasmess/tanaike/gasfakes.json +8 -0
- package/gasmess/tanaike/sample1.js +129 -0
- package/gasmess/tanaike/sample2.js +66 -0
- package/gasmess/tanaike/sample3.js +36 -0
- package/gasmess/tanaike/tempfolder.js +28 -0
- package/gasmess/tempfolder.js +29 -0
- package/package.json +5 -1
- package/shadowhelpers.js +0 -0
- package/src/index.js +2 -1
- package/src/services/advdocs/app.js +2 -1
- package/src/services/advdocs/docapis.js +4 -13
- package/src/services/advdocs/fakeadvdocs.js +604 -1
- package/src/services/advdocs/fakeadvdocuments.js +23 -13
- package/src/services/advdrive/app.js +1 -0
- package/src/services/advdrive/fakeadvdrive.js +6 -1
- package/src/services/advsheets/app.js +2 -1
- package/src/services/advsheets/fakeadvsheets.js +6 -1
- package/src/services/advsheets/fakeadvsheetsspreadsheets.js +1 -15
- package/src/services/advsheets/fakeadvsheetsvalues.js +7 -16
- package/src/services/advslides/app.js +2 -1
- package/src/services/advslides/fakeadvpresentations.js +51 -35
- package/src/services/advslides/fakeadvslides.js +56 -21
- package/src/services/advslides/slapis.js +4 -13
- package/src/services/common/fakeadvresource.js +1 -1
- package/src/services/documentapp/app.js +5 -1
- package/src/services/documentapp/appenderhelpers.js +266 -0
- package/src/services/documentapp/elementRegistry.js +22 -0
- package/src/services/documentapp/elementblasters.js +98 -0
- package/src/services/documentapp/elementhelpers.js +131 -0
- package/src/services/documentapp/elementoptions.js +246 -0
- package/src/services/documentapp/elements.js +14 -0
- package/src/services/documentapp/fakebody.js +78 -8
- package/src/services/documentapp/fakecontainerelement.js +176 -0
- package/src/services/documentapp/fakedocument.js +112 -35
- package/src/services/documentapp/fakedocumentapp.js +27 -7
- package/src/services/documentapp/fakedocumenttab.js +93 -0
- package/src/services/documentapp/fakeelement.js +242 -0
- package/src/services/documentapp/fakehorizontalrule.js +28 -0
- package/src/services/documentapp/fakepagebreak.js +29 -0
- package/src/services/documentapp/fakeparagraph.js +62 -0
- package/src/services/documentapp/fakerange.js +41 -0
- package/src/services/documentapp/fakerangebuilder.js +62 -0
- package/src/services/documentapp/fakerangeelement.js +31 -0
- package/src/services/documentapp/faketab.js +60 -0
- package/src/services/documentapp/faketable.js +52 -0
- package/src/services/documentapp/faketablecell.js +38 -0
- package/src/services/documentapp/faketablerow.js +46 -0
- package/src/services/documentapp/faketext.js +31 -0
- package/src/services/documentapp/nrhelpers.js +72 -0
- package/src/services/documentapp/shadow.js +121 -0
- package/src/services/documentapp/shadowdocument.js +263 -0
- package/src/services/driveapp/app.js +3 -2
- package/src/services/driveapp/fakedriveapp.js +2 -5
- package/src/services/enums/docsenums.js +155 -0
- package/src/services/enums/slidesenums.js +420 -0
- package/src/services/scriptapp/app.js +14 -5
- package/src/services/session/app.js +1 -1
- package/src/services/slidesapp/app.js +23 -0
- package/src/services/slidesapp/fakepresentation.js +47 -0
- package/src/services/slidesapp/fakeslidesapp.js +109 -0
- package/src/services/spreadsheetapp/app.js +3 -4
- package/src/services/spreadsheetapp/fakesheet.js +644 -81
- package/src/services/spreadsheetapp/fakesheetrange.js +1106 -603
- package/src/services/spreadsheetapp/fakespreadsheet.js +315 -278
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +115 -100
- package/src/services/spreadsheetapp/faketextfinder.js +275 -0
- package/src/services/spreadsheetapp/sheetrangehelpers.js +1 -1
- package/src/services/stores/app.js +3 -3
- package/src/services/urlfetchapp/app.js +9 -9
- package/src/services/utilities/app.js +2 -2
- package/src/support/docscacher.js +0 -28
- package/src/support/fakegasenum.js +35 -0
- package/src/support/fetchcacher.js +29 -0
- package/src/support/filecache.js +1 -1
- package/src/support/helpers.js +9 -1
- package/src/support/proxies.js +1 -0
- package/src/support/sheetscacher.js +9 -0
- package/src/support/slidescacher.js +9 -0
- package/src/support/sxdocs.js +9 -9
- package/src/support/sxdrive.js +4 -4
- package/src/support/sxsheets.js +4 -4
- package/src/support/sxslides.js +61 -0
- package/src/support/syncit.js +42 -81
- package/src/support/url.js +14 -0
- package/src/support/workersync/sxfunctions.js +14 -8
- package/togas.bash +3 -10
- package/src/support/sheetscache.js +0 -70
package/README.md
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# A proof of concept implementation of Apps Script Environment on Node
|
|
2
2
|
|
|
3
|
-
I use clasp/vscode to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and fowards to the GAS IDE going while testing. I set myself the ambition of implementing fake version of the GAS runtime environment on Node so I could at least do some testing locally.
|
|
3
|
+
I use clasp/vscode to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and fowards to the GAS IDE going while testing. I set myself the ambition of implementing fake version of the GAS runtime environment on Node so I could at least do some testing and debugging of Apps Scripts locally on Node.
|
|
4
4
|
|
|
5
|
-
This is a proof of concept so I've implemented a subset of number of services and methods
|
|
5
|
+
This is a proof of concept so I've implemented a growing subset of number of services and methods. There are a rigorous set of tests for all emulated classes and methods to make sure the same code produces the same result on both Node and Apps Script. Please report any inconsistencies in the issues of this repo.
|
|
6
6
|
|
|
7
|
-
## progress
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Getting started
|
|
8
|
+
## Getting started as a package user
|
|
12
9
|
|
|
13
10
|
You can get the package from npm
|
|
14
11
|
|
|
@@ -16,89 +13,48 @@ You can get the package from npm
|
|
|
16
13
|
npm i @mcpher/gas-fakes
|
|
17
14
|
```
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
Collaborators should fork the repo and use the local versions of these files - see [collaborators info](collaborators.md).
|
|
20
17
|
|
|
21
18
|
### Use exactly the same code as in Apps Script
|
|
22
19
|
|
|
23
|
-
Just as on Apps Script, everything is executed synchronously so you don't need to bother with handling Promises/async/await.
|
|
20
|
+
Just as on Apps Script, everything is executed synchronously so you don't need to bother with handling Promises/async/await. Just write normal Apps Script code. Usually you would have an associated App Script project if that's your eventual target, but it's not essential that you do. You can get started right away on Node.
|
|
24
21
|
|
|
25
|
-
### Where to use (and not use)
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
### Cloud project
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
You don't have access to GAS maintained cloud projects from Node, so you'll need to create a GCP project to use locally (or you can use it on Apps Script too if you prefer) that has the workspace APIs enabled (Drive, Docs, Sheets etc).
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
### .env and shell script helpers
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
In order to duplicate the OAuth management handled by GAS, we'll use Application Default Credentials. I've provided a handy shell that will take care of all this for you.
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
- Get this [folder](https://github.com/brucemcpherson/gas-fakes/tree/main/shells) into the ./shells folder of your project.
|
|
32
|
+
- Get this [env template](https://github.com/brucemcpherson/gas-fakes/blob/main/shells/.env.template) and copy it/add it to your .env file in your project
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
#### Application default credentials
|
|
38
35
|
|
|
39
|
-
|
|
36
|
+
At the very least you need to add the gcp project id you'll be using for testing, plus the id of some file you have access to - this'll be used to check that you have set up ADC properly.
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
There are other things in the .env-template you can ignore unless you're planning to run the test suite. More information on that is in [collaborators info](collaborators.md)
|
|
42
39
|
|
|
40
|
+
|
|
41
|
+
These should be in your .env file to enable ADC authentication. The purpose of the DRIVE_TEST_FILE_ID is so that the script can check you've enabled ADC correctly by pinging a file you have access to. The GCP_PROJECT_ID is required as it will be used by gas-fakes to access the workspace apis on your behalf.
|
|
43
42
|
```
|
|
44
|
-
#
|
|
43
|
+
# must set these
|
|
45
44
|
GCP_PROJECT_ID="add your gcp project id here"
|
|
46
45
|
DRIVE_TEST_FILE_ID="add the id of some test file you have access to here"
|
|
47
46
|
|
|
48
47
|
# we'll use the default config for application default credentials
|
|
48
|
+
# probably dont need to change these
|
|
49
49
|
AC=default
|
|
50
50
|
# these are the scopes set by default - take some of these out if you want to minimize access
|
|
51
51
|
DEFAULT_SCOPES="https://www.googleapis.com/auth/userinfo.email,openid,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/sqlservice.login"
|
|
52
52
|
EXTRA_SCOPES=",https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/spreadsheets"
|
|
53
53
|
|
|
54
|
-
# everything below here is required for the test suite if you are using it otherwise you can ignore everything below here
|
|
55
|
-
# you can ask me for access to the shared files if you are running the test suite, or you can create your own versions
|
|
56
|
-
|
|
57
|
-
# set your email address to test various apis
|
|
58
|
-
SHARED_FILE_OWNER="bruce@mcpher.com"
|
|
59
|
-
EMAIL="bruce@mcpher.com"
|
|
60
|
-
TIMEZONE="Europe/London"
|
|
61
|
-
TEST_LOCALE="en"
|
|
62
|
-
OWNER_NAME="Bruce Mcpherson"
|
|
63
|
-
|
|
64
|
-
# these are files that are used to test some drive features
|
|
65
|
-
TEST_BORDERS_ID="1hRGdrYHEPixXTuQLeL3Z0qGRZVs_8ojMIm6D4KrCh1o"
|
|
66
|
-
TEST_AIRPORTS_ID="1h9IGIShgVBVUrUjjawk5MaCEQte_7t32XeEP1Z5jXKQ"
|
|
67
|
-
TEST_AIRPORTS_NAME="airport list"
|
|
68
|
-
TEST_FOLDER_NAME="math"
|
|
69
|
-
TEST_FOLDER_FILES=3
|
|
70
|
-
TEST_FOLDER_ID="1Zww9oCTFR7zYcUYXxd70yQr3sw6VdLG-"
|
|
71
|
-
TEXT_FILE_NAME="fake.txt"
|
|
72
|
-
TEXT_FILE_ID="1142Vn7W-pGl5nWLpUSkpOB82JDiz9R6p"
|
|
73
|
-
TEXT_FILE_TYPE="text/plain"
|
|
74
|
-
TEXT_FILE_CONTENT="foo is not bar"
|
|
75
|
-
BLOB_NAME="foo.txt"
|
|
76
|
-
BLOB_TYPE="text/plain"
|
|
77
|
-
TEST_SHEET_ID="1DlKpVVYCrCPNfRbGsz6N_K3oPTgdC9gQIKi0aNb42uI"
|
|
78
|
-
TEST_SHEET_NAME="sharedlibraries"
|
|
79
|
-
PUBLIC_SHARE_FILE_ID="1OFJk38kW9TRrEf-B9F1gTZk2uLV-ZSpR"
|
|
80
|
-
SHARED_FILE_ID="1uz4cxEDxtQzu0cBb1B4h6fsjgWy7hNFf"
|
|
81
|
-
PDF_ID="1v5kJ5SOY2nu3DI1LKwALb3seaBpF3kWu"
|
|
82
|
-
SCRATCH_VIEWER="viewer@mcpher.com"
|
|
83
|
-
SCRATCH_EDITOR="editor@mcpher.com"
|
|
84
|
-
SCRATCH_B_VIEWER="viewer2@mcpher.com"
|
|
85
|
-
SCRATCH_B_EDITOR="editor2@mcpher.com"
|
|
86
|
-
|
|
87
|
-
# these are parameters for testing
|
|
88
|
-
MIN_ROOT_PDFS=20
|
|
89
|
-
MIN_PDFS=400
|
|
90
|
-
MIN_FOLDERS_ROOT=110
|
|
91
|
-
SKIP_SINGLE_PARENT=1
|
|
92
|
-
|
|
93
|
-
ZIP_TYPE="application/zip"
|
|
94
|
-
KIND_DRIVE="drive#file"
|
|
95
|
-
|
|
96
|
-
RANDOM_IMAGE="https://picsum.photos/200"
|
|
97
|
-
API_URL="http://suggestqueries.google.com/complete/search?client=chrome&hl=en&q=trump"
|
|
98
|
-
API_TYPE="text/javascript"
|
|
99
|
-
CLEAN=1
|
|
100
54
|
```
|
|
101
55
|
|
|
56
|
+
- goto ./shells and execute sp.sh
|
|
57
|
+
|
|
102
58
|
### OAuth
|
|
103
59
|
|
|
104
60
|
There's 2 pieces to this solution.
|
|
@@ -121,13 +77,15 @@ EXTRA_SCOPES=",https://www.googleapis.com/auth/drive,https://www.googleapis.com/
|
|
|
121
77
|
|
|
122
78
|
#### Manifest file
|
|
123
79
|
|
|
80
|
+
If you have an associated apps script project, you'll probably be using clasp to sync with the apps script IDE, and you'll have an appsscript.json available in your project folder
|
|
81
|
+
|
|
124
82
|
**gas-fakes** reads the manifest file to see which scopes you need in your project, uses the Google Auth library to attempt to authorizes them and has `ScriptApp.getOauthToken()` return a sufficiently specced token, just as the GAS environment does. Just make sure you have an `appsscript.json` in the same folder as your main script.
|
|
125
83
|
|
|
126
84
|
Now you can execute this and it will set up your ADC to be able to run any services that require the scopes you add.
|
|
127
85
|
|
|
128
86
|
##### note
|
|
129
87
|
|
|
130
|
-
Although you may be tempted to add `https://www.googleapis.com/auth/script.external_request`, it's not necessary for the ADC and in fact will generate an error. You will of course need it in your Apps script manifest.
|
|
88
|
+
Although you may be tempted to add `https://www.googleapis.com/auth/script.external_request`, it's not necessary for the ADC and in fact will generate an error. You will of course need it in your Apps script manifest. Same goes for "https://www.googleapis.com/auth/documents" and "https://www.googleapis.com/auth/documents"
|
|
131
89
|
|
|
132
90
|
### Settings
|
|
133
91
|
|
|
@@ -153,818 +111,18 @@ Optionally, gasfakes.json holds various location and behavior parameters to info
|
|
|
153
111
|
| properties | string | /tmp/gas-fakes/properties | gas-fakes uses a local file to emulate apps script's PropertiesService. This is where it should put the files. You may want to put it somewhere other than /tmp to avoid accidental deletion, but don't put it in a place that'll get commited to public git repo |
|
|
154
112
|
| scriptId | string | from clasp, or some random value | If you have a clasp file, it'll pick up the scriptId from there. If not you can enter your scriptId manually, or just leave it to create a fake one. It's use for the moment is to return something useful from ScriptApp.getScriptId() and to partition the cache and properties stores |
|
|
155
113
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
## Approach
|
|
159
|
-
|
|
160
|
-
Google have not made details about the GAS run time public (as far as I know). What we do know is that it used to run on a Java based JavaScript emulator [Rhino](https://ramblings.mcpher.com/gassnippets2/what-javascript-engine-is-apps-script-running-on/) but a few years ago moved to a V8 runtime. Beyond that, we don't know anything much other than it runs on Google Servers somewhere.
|
|
161
|
-
|
|
162
|
-
There were 3 main sticky problems to overcome to get this working
|
|
163
|
-
|
|
164
|
-
- GAS is entirely synchronous, whereas the replacement calls to Workspace APIS on Node are all asynchrounous.
|
|
165
|
-
- GAS handles OAuth initialization from the manifest file automatically, whereas we need some additional coding or alternative approaches on Node.
|
|
166
|
-
- The service singletons (eg. DriveApp) are all intialized and available in the global space automatically, whereas in Node they need some post AUTH intialization, sequencing intialization and exposure.
|
|
167
|
-
- GAS iterators aren't the same as standard iterators, as they have a hasNext() method and don't behave in the same way.
|
|
168
|
-
|
|
169
|
-
Beyond that, implementation is just a lot of busy work. If you are interested, here's how I've dealt with these 3 problems.
|
|
170
|
-
|
|
171
|
-
### Sync versus Async
|
|
172
|
-
|
|
173
|
-
Although Apps Script supports async/await/promise syntax, it operates in blocking mode. I didn't really want to have to insist on async coding in code targeted at GAS, so I needed to find a way to emulate what the GAS environment probably does.
|
|
174
|
-
|
|
175
|
-
Since asynchonicity is fundamental to Node, there's no real simple way to convert async to sync. However, there is such a thing as a [child-process](https://nodejs.org/api/child_process.html#child-process) which you can start up to run things, and it features an [execSync](https://nodejs.org/api/child_process.html#child_processexecsynccommand-options) method which delays the return from the child process until the promise queue is all settled. So the simplest solution is to run an async method in a child process, wait till it's done, and return the results synchronously. I found that [Sindre Sorhus](https://github.com/sindresorhus) uses this approach with [make-synchronous](https://github.com/sindresorhus/make-synchronous).However, runnng up a child process in Node is pretty expensive and slow, and each subprocess has to reimport the google apis and go through a reauth chain which can take up to 1.5 secs per call.
|
|
176
|
-
|
|
177
|
-
#### Worker Update
|
|
178
|
-
|
|
179
|
-
I'm now upgrading to use a worker thread to handle all activities that need to be performed synchronously. It's a lot more tricky to implement and handle exceptions, but worth the effort.
|
|
180
|
-
|
|
181
|
-
- The worker thread only needs to be authed once on initialization and retains state between each call.
|
|
182
|
-
- Control to shared memory is via [Node Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) which gives a mutex style control.
|
|
183
|
-
- Node has built in [worker threads](https://nodejs.org/api/worker_threads.html), so there's no need for any external libraries
|
|
184
|
-
- Only arguments that can be stringified can be passed to and from a worker - but this is the same limitiation as passing arguments to a subprocess
|
|
185
|
-
- To avoid grabbing too much shared memory,I use a temporary file to pass huge amounts of data - but this will be a rarish exception.
|
|
186
|
-
- There are lot of async gotchas with workers so your async handling needs to be very precise. I spent a lot of time trying to track down potentially unsettled promises only to discover that worker.unref() is required to prevent the worker from stopping the main process exiting.
|
|
187
|
-
- console.log doesnt work reliable in a worker, even if you redirect the workers stdout & stder
|
|
188
|
-
|
|
189
|
-
```js
|
|
190
|
-
worker.stdout.pipe(process.stdout);
|
|
191
|
-
worker.stderr.pipe(process.stderr);
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
This is because console.log is async, and never shows. You need a sync version of console.log - as implemented in `./src/support/workersync/synclogger`
|
|
195
|
-
|
|
196
|
-
The result is a dramatic speed up over the subprocess approach (x5). So much so, that I had to add exponential backup to the worker threads to overcome quota limits on the workspace APIS to be able to run the test suite. Having said that it is still very much slower (x4 but variable) than most of the same calls in Apps Script - which appears to feature some mixture of in memory shadowing, caching and api call bundling - which I don't intend to mimic in this fake enironment (for now anyway) as this is not about improving the speed of Apps Script but about emulating it.
|
|
197
|
-
|
|
198
|
-
It's additionally slowed down because there are an unnatural amount of rapid, consecutive calls in the test suite which means that we get an unnatural amount of delay waiting for a quota window (sometimes as much as 15 seconds) added due to exponential back off delays when running the full test suite (I assume Apps script doesn't have the same quota restrictions). In normal operation this is unlikely to be problem.
|
|
199
|
-
|
|
200
|
-
### Global intialization
|
|
201
|
-
|
|
202
|
-
This was a little problematic to sequence, but I wanted to make sure that any GAS services being imitated were available and initialized on the Node side, just as they are in GAS. At the time of writing these services and classes are partially implemented.
|
|
203
|
-
|
|
204
|
-
Only a subset of methods are currently available for some of them - the rest are work in progress. My approach is to start with a little bit of each service to prove feasibility and provide a base to build on.
|
|
205
|
-
|
|
206
|
-
v1.0.13
|
|
207
|
-
|
|
208
|
-
- `DriveApp` - 50%
|
|
209
|
-
- `ScriptApp` - almost all
|
|
210
|
-
- `UrlFetchApp` - 80%
|
|
211
|
-
- `Utilities` - almost all
|
|
212
|
-
- `Sheets` - 50%
|
|
213
|
-
- `SpreadsheetApp` - 60%
|
|
214
|
-
- `CacheService` - 80%
|
|
215
|
-
- `PropertiesService` - 80%
|
|
216
|
-
- `Session` - almost all
|
|
217
|
-
- `Blob` - all
|
|
218
|
-
- `User` - all
|
|
219
|
-
- `Drive (Advanced Service)` - 40%
|
|
220
|
-
- `Sheets (Advanced Service)` - 60%
|
|
221
|
-
- `Slides (Advanced Service)` - 2%
|
|
222
|
-
- `Docs (Advanced Service)` - 75%
|
|
223
|
-
- `DocumentApp` - 10%
|
|
224
|
-
- `SlidesApp` - placeholder
|
|
225
|
-
|
|
226
|
-
### Testing coverage
|
|
227
|
-
|
|
228
|
-
Tests for all methods are added as we go to the cumulative unit tests and run on both Apps Script and Node. The goal is to try to get the behavior as exactly equivalent as possible. There are currently almost 4000 active tests.
|
|
229
|
-
|
|
230
|
-
#### Proxies and globalThis
|
|
231
|
-
|
|
232
|
-
Each service has a FakeClass but I needed the Auth cycle to be initiated and done before making them public. Using a proxy was the simplest approach.
|
|
233
|
-
|
|
234
|
-
In short, the service is registered as an empty object, but when any attempt is made to access it actually returns a different object which handles the request. In the `ScriptApp` example, `ScriptApp` is an empty object, but accessing `ScriptApp.getOAuthToken()` returns an Fake `ScriptApp` object which gets initialized if you try to access it.
|
|
235
|
-
|
|
236
|
-
There's also a test available to see if you are running in GAS or on Node - `ScriptApp.isFake`. In fact this method 'isFake' is available on any of the implemented services, eg `DriveApp.isFake`.
|
|
237
|
-
|
|
238
|
-
### Iterators
|
|
239
|
-
|
|
240
|
-
An iterator created by a generator does not have a `hasNext()` function, whereas GAS iterators do. To get round this, I use a regular Node iterator, but with a wrapper so the constructor actually gets the first one, and `next()` uses the value we've already peeked at.
|
|
241
|
-
|
|
242
|
-
### Cache and Property services
|
|
243
|
-
|
|
244
|
-
These are currently implemented using [keyv](https://github.com/jaredwray/keyv) with storage adaptor [keyv-file](https://github.com/zaaack/keyv-file).The `gasfakes.json` file is used to commiicate where these files should be. I've gone for local file storage rather than something like redis to avoid adding local service requirements, but keyv takes a wide range of storage adaptors if you want to do something fancier. A small modificaion to kv.js is all you need.
|
|
245
|
-
|
|
246
|
-
#### Script, user and document store varieties
|
|
247
|
-
|
|
248
|
-
All 3 are supported for both properties and cache.
|
|
249
|
-
|
|
250
|
-
##### scriptId
|
|
251
|
-
|
|
252
|
-
The local version may have no knowledge of the Apps ScriptId. If you are using clasp, it's picked up from the .clasp.json file. However if you are not using clasp, or want to use something else, you can set the scriptId in `gasfakes.json`, otherwise it'll create a fake id use that - but that means that each store will be different each time you run it, so the best approach is to add a value unique to the group of scripts that want to share the same stores in gas-fakes.json. All property and cache stores use the scriptId to partition data.
|
|
253
|
-
|
|
254
|
-
##### userId
|
|
255
|
-
|
|
256
|
-
The userId is extracted from an accessToken and will match the id derived from Application Default Credentials. This means that you can logon as a different user to test user data isolation. All user level property and cache stores use the scriptId and userId to partition data.
|
|
257
|
-
|
|
258
|
-
##### documentId
|
|
259
|
-
|
|
260
|
-
The documentId is only meaningful if you are working on a container bound scrip. We use the the documentId property of gasfakes.json to identify a container file. All document level property and cache stores use the scriptId and documentId to partition data.
|
|
261
|
-
|
|
262
|
-
### Settings and temporary files
|
|
263
|
-
|
|
264
|
-
As you will have noticed, there are various local support files for props/caching etc. Be careful that these do not get committed to a public repo if you are adding sensitive values to your stores. Note that the real user Id is not used when creating files, but rather an encrypted version of it. This avoids real user ids being revealed in your file system.
|
|
265
|
-
|
|
266
|
-
## Debugging
|
|
267
|
-
|
|
268
|
-
For conversion of async to sync, I'm spawing a subprocess using [make-synchronous](https://github.com/sindresorhus/make-synchronous). Out of the box it inherits the Node Options from the main process, so that means it'll try to run each subprocess in debug mode also. Bringing up and down the debugger each time takes forever, so I've temporaily modified my local version of make-synchronous to drop the debug inheritance.
|
|
269
|
-
|
|
270
|
-
If this makes it to the repo we can start to use it from there - see issue https://github.com/sindresorhus/make-synchronous/issues/14
|
|
271
|
-
|
|
272
|
-
## Noticed differences
|
|
273
|
-
|
|
274
|
-
In the main, these will be slight differences in error message text, which I'll normalize over time, or where Apps Script has a fundamental obstacle. Please report any differences in behavior you find in the repo issues.
|
|
275
|
-
|
|
276
|
-
### Tradeoffs
|
|
277
|
-
|
|
278
|
-
I've come across various Apps Script bugs/issues as I work through this which I've reported to the GAS team, and added workarounds in the gas fakes code - not sure at this point whether to duplicate the buggy behavior or simulate what would seem to be the correct one. This is not a complete list, so any things you come across please use the issues in the repo to report.
|
|
279
|
-
|
|
280
|
-
## Oddities
|
|
281
|
-
|
|
282
|
-
Just a few things I've come across when digging into the differences between what the sheets API and Apps Script do. Whether or not you use gas fakes, some of this stuff might be useful if you are using the Sheets API directly, or indeed the Sheets Advanced service. I'll just make a growing list of stuff I've found, in no particular order.
|
|
283
|
-
|
|
284
|
-
### Note to collaborators
|
|
285
|
-
|
|
286
|
-
Gemini code assist can be a very helpful for the busy work, but there a huge number of inconsistencies between what it believes to be the documentation and the actual real world, so if Gemini starts flailing take over early.
|
|
287
|
-
|
|
288
|
-
You eventually have to dig into the docs yourself to track down why something Gemini advised isn't working.
|
|
289
|
-
|
|
290
|
-
Gemini can also write test cases, but it tends to miss adding edge cases, so don't rely on Gemini completely for that, and always get the tests working Apps Script side first to ensure it is behaving as expected (often it's not) - after all that's what we're trying to emulate.
|
|
291
|
-
|
|
292
|
-
### Colors
|
|
293
|
-
|
|
294
|
-
#### Named Colors
|
|
295
|
-
|
|
296
|
-
In addition to CSS hex notation (e.g., `#ff0000`), Apps Script methods like `Range.setBackground()` and `Range.setFontColor()` also accept standard CSS color names. The fake environment supports all 147 standard names, which are treated case-insensitively.
|
|
297
|
-
|
|
298
|
-
| Name | Hex Value |
|
|
299
|
-
| ---------------------- | --------- |
|
|
300
|
-
| `aliceblue` | `#f0f8ff` |
|
|
301
|
-
| `antiquewhite` | `#faebd7` |
|
|
302
|
-
| `aqua` | `#00ffff` |
|
|
303
|
-
| `aquamarine` | `#7fffd4` |
|
|
304
|
-
| `azure` | `#f0ffff` |
|
|
305
|
-
| `beige` | `#f5f5dc` |
|
|
306
|
-
| `bisque` | `#ffe4c4` |
|
|
307
|
-
| `black` | `#000000` |
|
|
308
|
-
| `blanchedalmond` | `#ffebcd` |
|
|
309
|
-
| `blue` | `#0000ff` |
|
|
310
|
-
| `blueviolet` | `#8a2be2` |
|
|
311
|
-
| `brown` | `#a52a2a` |
|
|
312
|
-
| `burlywood` | `#deb887` |
|
|
313
|
-
| `cadetblue` | `#5f9ea0` |
|
|
314
|
-
| `chartreuse` | `#7fff00` |
|
|
315
|
-
| `chocolate` | `#d2691e` |
|
|
316
|
-
| `coral` | `#ff7f50` |
|
|
317
|
-
| `cornflowerblue` | `#6495ed` |
|
|
318
|
-
| `cornsilk` | `#fff8dc` |
|
|
319
|
-
| `crimson` | `#dc143c` |
|
|
320
|
-
| `cyan` | `#00ffff` |
|
|
321
|
-
| `darkblue` | `#00008b` |
|
|
322
|
-
| `darkcyan` | `#008b8b` |
|
|
323
|
-
| `darkgoldenrod` | `#b8860b` |
|
|
324
|
-
| `darkgray` | `#a9a9a9` |
|
|
325
|
-
| `darkgreen` | `#006400` |
|
|
326
|
-
| `darkgrey` | `#a9a9a9` |
|
|
327
|
-
| `darkkhaki` | `#bdb76b` |
|
|
328
|
-
| `darkmagenta` | `#8b008b` |
|
|
329
|
-
| `darkolivegreen` | `#556b2f` |
|
|
330
|
-
| `darkorange` | `#ff8c00` |
|
|
331
|
-
| `darkorchid` | `#9932cc` |
|
|
332
|
-
| `darkred` | `#8b0000` |
|
|
333
|
-
| `darksalmon` | `#e9967a` |
|
|
334
|
-
| `darkseagreen` | `#8fbc8f` |
|
|
335
|
-
| `darkslateblue` | `#483d8b` |
|
|
336
|
-
| `darkslategray` | `#2f4f4f` |
|
|
337
|
-
| `darkslategrey` | `#2f4f4f` |
|
|
338
|
-
| `darkturquoise` | `#00ced1` |
|
|
339
|
-
| `darkviolet` | `#9400d3` |
|
|
340
|
-
| `deeppink` | `#ff1493` |
|
|
341
|
-
| `deepskyblue` | `#00bfff` |
|
|
342
|
-
| `dimgray` | `#696969` |
|
|
343
|
-
| `dimgrey` | `#696969` |
|
|
344
|
-
| `dodgerblue` | `#1e90ff` |
|
|
345
|
-
| `firebrick` | `#b22222` |
|
|
346
|
-
| `floralwhite` | `#fffaf0` |
|
|
347
|
-
| `forestgreen` | `#228b22` |
|
|
348
|
-
| `fuchsia` | `#ff00ff` |
|
|
349
|
-
| `gainsboro` | `#dcdcdc` |
|
|
350
|
-
| `ghostwhite` | `#f8f8ff` |
|
|
351
|
-
| `gold` | `#ffd700` |
|
|
352
|
-
| `goldenrod` | `#daa520` |
|
|
353
|
-
| `gray` | `#808080` |
|
|
354
|
-
| `green` | `#008000` |
|
|
355
|
-
| `greenyellow` | `#adff2f` |
|
|
356
|
-
| `grey` | `#808080` |
|
|
357
|
-
| `honeydew` | `#f0fff0` |
|
|
358
|
-
| `hotpink` | `#ff69b4` |
|
|
359
|
-
| `indianred` | `#cd5c5c` |
|
|
360
|
-
| `indigo` | `#4b0082` |
|
|
361
|
-
| `ivory` | `#fffff0` |
|
|
362
|
-
| `khaki` | `#f0e68c` |
|
|
363
|
-
| `lavender` | `#e6e6fa` |
|
|
364
|
-
| `lavenderblush` | `#fff0f5` |
|
|
365
|
-
| `lawngreen` | `#7cfc00` |
|
|
366
|
-
| `lemonchiffon` | `#fffacd` |
|
|
367
|
-
| `lightblue` | `#add8e6` |
|
|
368
|
-
| `lightcoral` | `#f08080` |
|
|
369
|
-
| `lightcyan` | `#e0ffff` |
|
|
370
|
-
| `lightgoldenrodyellow` | `#fafad2` |
|
|
371
|
-
| `lightgray` | `#d3d3d3` |
|
|
372
|
-
| `lightgreen` | `#90ee90` |
|
|
373
|
-
| `lightgrey` | `#d3d3d3` |
|
|
374
|
-
| `lightpink` | `#ffb6c1` |
|
|
375
|
-
| `lightsalmon` | `#ffa07a` |
|
|
376
|
-
| `lightseagreen` | `#20b2aa` |
|
|
377
|
-
| `lightskyblue` | `#87cefa` |
|
|
378
|
-
| `lightslategray` | `#778899` |
|
|
379
|
-
| `lightslategrey` | `#778899` |
|
|
380
|
-
| `lightsteelblue` | `#b0c4de` |
|
|
381
|
-
| `lightyellow` | `#ffffe0` |
|
|
382
|
-
| `lime` | `#00ff00` |
|
|
383
|
-
| `limegreen` | `#32cd32` |
|
|
384
|
-
| `linen` | `#faf0e6` |
|
|
385
|
-
| `magenta` | `#ff00ff` |
|
|
386
|
-
| `maroon` | `#800000` |
|
|
387
|
-
| `mediumaquamarine` | `#66cdaa` |
|
|
388
|
-
| `mediumblue` | `#0000cd` |
|
|
389
|
-
| `mediumorchid` | `#ba55d3` |
|
|
390
|
-
| `mediumpurple` | `#9370db` |
|
|
391
|
-
| `mediumseagreen` | `#3cb371` |
|
|
392
|
-
| `mediumslateblue` | `#7b68ee` |
|
|
393
|
-
| `mediumspringgreen` | `#00fa9a` |
|
|
394
|
-
| `mediumturquoise` | `#48d1cc` |
|
|
395
|
-
| `mediumvioletred` | `#c71585` |
|
|
396
|
-
| `midnightblue` | `#191970` |
|
|
397
|
-
| `mintcream` | `#f5fffa` |
|
|
398
|
-
| `mistyrose` | `#ffe4e1` |
|
|
399
|
-
| `moccasin` | `#ffe4b5` |
|
|
400
|
-
| `navajowhite` | `#ffdead` |
|
|
401
|
-
| `navy` | `#000080` |
|
|
402
|
-
| `oldlace` | `#fdf5e6` |
|
|
403
|
-
| `olive` | `#808000` |
|
|
404
|
-
| `olivedrab` | `#6b8e23` |
|
|
405
|
-
| `orange` | `#ffa500` |
|
|
406
|
-
| `orangered` | `#ff4500` |
|
|
407
|
-
| `orchid` | `#da70d6` |
|
|
408
|
-
| `palegoldenrod` | `#eee8aa` |
|
|
409
|
-
| `palegreen` | `#98fb98` |
|
|
410
|
-
| `paleturquoise` | `#afeeee` |
|
|
411
|
-
| `palevioletred` | `#db7093` |
|
|
412
|
-
| `papayawhip` | `#ffefd5` |
|
|
413
|
-
| `peachpuff` | `#ffdab9` |
|
|
414
|
-
| `peru` | `#cd853f` |
|
|
415
|
-
| `pink` | `#ffc0cb` |
|
|
416
|
-
| `plum` | `#dda0dd` |
|
|
417
|
-
| `powderblue` | `#b0e0e6` |
|
|
418
|
-
| `purple` | `#800080` |
|
|
419
|
-
| `red` | `#ff0000` |
|
|
420
|
-
| `rosybrown` | `#bc8f8f` |
|
|
421
|
-
| `royalblue` | `#4169e1` |
|
|
422
|
-
| `saddlebrown` | `#8b4513` |
|
|
423
|
-
| `salmon` | `#fa8072` |
|
|
424
|
-
| `sandybrown` | `#f4a460` |
|
|
425
|
-
| `seagreen` | `#2e8b57` |
|
|
426
|
-
| `seashell` | `#fff5ee` |
|
|
427
|
-
| `sienna` | `#a0522d` |
|
|
428
|
-
| `silver` | `#c0c0c0` |
|
|
429
|
-
| `skyblue` | `#87ceeb` |
|
|
430
|
-
| `slateblue` | `#6a5acd` |
|
|
431
|
-
| `slategray` | `#708090` |
|
|
432
|
-
| `slategrey` | `#708090` |
|
|
433
|
-
| `snow` | `#fffafa` |
|
|
434
|
-
| `springgreen` | `#00ff7f` |
|
|
435
|
-
| `steelblue` | `#4682b4` |
|
|
436
|
-
| `tan` | `#d2b48c` |
|
|
437
|
-
| `teal` | `#008080` |
|
|
438
|
-
| `thistle` | `#d8bfd8` |
|
|
439
|
-
| `tomato` | `#ff6347` |
|
|
440
|
-
| `turquoise` | `#40e0d0` |
|
|
441
|
-
| `violet` | `#ee82ee` |
|
|
442
|
-
| `wheat` | `#f5deb3` |
|
|
443
|
-
| `white` | `#ffffff` |
|
|
444
|
-
| `whitesmoke` | `#f5f5f5` |
|
|
445
|
-
| `yellow` | `#ffff00` |
|
|
446
|
-
| `yellowgreen` | `#9acd32` |
|
|
447
|
-
|
|
448
|
-
##### rebeccapurple
|
|
449
|
-
|
|
450
|
-
This is an interesting html color name that apps script does not support, so I've omitted that from the color name support. To learn more about this color name see - https://medium.com/@valgaze/the-hidden-purple-memorial-in-your-web-browser-7d84813bb416
|
|
451
|
-
|
|
452
|
-
#### Banding Themes
|
|
453
|
-
|
|
454
|
-
The colors used for banding themes can change over time with UI updates from Google. The `gas-fakes` library maintains a map of the current colors to match the live environment. The `Banding Theme Colors Verification` test in `testsheetssets.js` is used to validate these.
|
|
455
|
-
|
|
456
|
-
| Theme | Header | First Band | Second Band | Footer |
|
|
457
|
-
| ------------- | --------- | ---------- | ----------- | --------- |
|
|
458
|
-
| `LIGHT_GREY` | `#bdbdbd` | `#ffffff` | `#f3f3f3` | `#dedede` |
|
|
459
|
-
| `CYAN` | `#4dd0e1` | `#ffffff` | `#e0f7fa` | `#a2e8f1` |
|
|
460
|
-
| `GREEN` | `#63d297` | `#ffffff` | `#e7f9ef` | `#afe9ca` |
|
|
461
|
-
| `YELLOW` | `#f7cb4d` | `#ffffff` | `#fef8e3` | `#fce8b2` |
|
|
462
|
-
| `ORANGE` | `#f46524` | `#ffffff` | `#ffe6dd` | `#ffccbc` |
|
|
463
|
-
| `BLUE` | `#5b95f9` | `#ffffff` | `#e8f0fe` | `#acc9fe` |
|
|
464
|
-
| `TEAL` | `#26a69a` | `#ffffff` | `#ddf2f0` | `#8cd3cd` |
|
|
465
|
-
| `GREY` | `#78909c` | `#ffffff` | `#ebeff1` | `#bbc8ce` |
|
|
466
|
-
| `BROWN` | `#cca677` | `#ffffff` | `#f8f2eb` | `#e6d3ba` |
|
|
467
|
-
| `LIGHT_GREEN` | `#8bc34a` | `#ffffff` | `#eef7e3` | `#c4e2a0` |
|
|
468
|
-
| `INDIGO` | `#8989eb` | `#ffffff` | `#e8e7fc` | `#c4c3f7` |
|
|
469
|
-
| `PINK` | `#e91d63` | `#ffffff` | `#fddce8` | `#f68ab0` |
|
|
470
|
-
|
|
471
|
-
### Fake classes
|
|
472
|
-
|
|
473
|
-
Most Apps script classes will map to a separate fake class file - sometimes more than one. Many of the methods in large classes are generated from various specification files, but the more complex ones and the ones with weird behavior are directly written as methods in the class.
|
|
474
|
-
|
|
475
|
-
i've tried to avoid adding properties and methods that don't exist in the emulated class, but sometimes it's necessary to have private methods and properties. Since Apps Script doesn't support private properties, I've decided to simply identify these with a leading pair of underscores, eg this.\_\_myProperty.
|
|
476
|
-
|
|
477
|
-
Although not strictly necessary to avoid real private propertues, since these Fake classes will exist only on Node, I wanted to keep code comaptible with Apps Script.
|
|
478
|
-
|
|
479
|
-
Most classes have a new method -- eg `newFakeClass(args)`. It's best to use this rather than `new FakeClass(args)`, since they each wrap the instance created in a proxy that detects attempts to access non existent properties, or indeed to set any properties other than private ones. Very handy when debugging.
|
|
480
|
-
|
|
481
|
-
### Formats and styles
|
|
482
|
-
|
|
483
|
-
When getting formats with the sheets API, there are 2 types
|
|
484
|
-
|
|
485
|
-
- userEnteredFormat - any formats a user (or an apps script function) has explicitly set
|
|
486
|
-
- effectiveFormat - what rendered format actually looks like
|
|
487
|
-
|
|
488
|
-
This means that sometimes, for example, a font might be red in the UI, but Apps Script reports it as black. This is because Apps Script uses the userEnteredFormat exclusively (I think). I've implemented the same in Gas Fakes. To get the effectiveFormat, you'll need to use the Fake Advanced Sheets service, just as you would in Apps Script.
|
|
489
|
-
|
|
490
|
-
### Values
|
|
491
|
-
|
|
492
|
-
Just as with Formats, the actual value rendered might be different than the value stored. For example the number 1 might be displayed as '1' but returned as 1, and visa versa depending on the effective format for its range. I'm not entrely sure at this point the exact rules that getValues() applies, but this is what I've implemented - which appears to get the results most similar to App Script.
|
|
493
|
-
|
|
494
|
-
Here is how I've implemented getting and setting values.
|
|
495
|
-
|
|
496
|
-
- getValues() uses { valueRenderOption: 'UNFORMATTED_VALUE' }
|
|
497
|
-
- setValues() uses { valueInputOption: "RAW" } (as opposed to 'USER_ENTERED')
|
|
498
|
-
- getDisplayValues() { valueRenderOption: 'FORMATTED_VALUE' }
|
|
499
|
-
|
|
500
|
-
### Data Validation
|
|
501
|
-
|
|
502
|
-
There's quite a few oddities in Data Validation, which turned out to be the most complicated topic I've tackled at the time of writing.
|
|
503
|
-
|
|
504
|
-
#### Criteria types
|
|
505
|
-
|
|
506
|
-
A few of the criteria types differ between the Sheets API and Apps Script - for example TEXT_IS_VALID_EMAIL on GAS is equivalent to TEXT_IS_EMAIL on the API, and VALUE_IN_LIST is equivalent to ONE_OF_LIST and a few others. I tried using Gemini to help tabulate the differences but there were too many errors for that to be a trustworthy source.
|
|
507
|
-
|
|
508
|
-
The file 'fakedatavalidationcriteria.js' has a list of the final mappings between the 2.
|
|
509
|
-
|
|
510
|
-
#### Relative dates
|
|
511
|
-
|
|
512
|
-
Both the sheets API and GAS can return either relative dates or actual dates. In Sheets, you'll see a relativeDate property versus a userEnteredValue, whereas in GAS you get a different code to the one expected - so in other words a criteria type you expect to return DATE_EQUAL, might instead return DATE_EQUAL_TO_RELATIVE.
|
|
513
|
-
|
|
514
|
-
##### Setting a relative date
|
|
515
|
-
|
|
516
|
-
There are no methods in Apps Script to actually set relative dates in Data Validation - for example you'd expect a method such as requireDateEqualToRelative to exist - but it doesn't - to set you'd need to use the advanced sheets service or the withCriteria method. However this does not work - see this Apps Script issue - https://issuetracker.google.com/issues/418495831
|
|
517
|
-
|
|
518
|
-
Not all date validations have related RELATIVE versions. See later section for details.
|
|
519
|
-
|
|
520
|
-
In GAS (and of course also with GasFakes), in theory you would set a relative date like this, which gives the appearance of working, but in fact does nothing. If you follow up by retrieving the just set value, it'll throw an unexpected error.
|
|
521
|
-
|
|
522
|
-
```js
|
|
523
|
-
const rule = SpreadsheetApp.newDataValidation()
|
|
524
|
-
.withCriteria(SpreadsheetApp.DataValidationCriteria.DATE_EQUAL_TO_RELATIVE, [
|
|
525
|
-
SpreadsheetApp.RelativeDate.TODAY,
|
|
526
|
-
])
|
|
527
|
-
.build();
|
|
528
|
-
const range = sheet.getRange("b30");
|
|
529
|
-
range.setDataValidation(rule);
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
Because this doesn't work in GAS, I'm not at this point sure whether to handle this or throw an error. Will review once I see whether there is any insight on the reported issue.
|
|
533
|
-
|
|
534
|
-
##### Getting a relative date
|
|
535
|
-
|
|
536
|
-
You can of course set a limited set of relative Data Validation via the UI, and GAS supports returning its content. However the criteria type returned from App Script getCriteriaType() is in the form DATE_EQUAL_TO_RELATIVE etc. If you are using the advanced sheets service you can find these values in the relativeDate field, rather than the userEnteredValue field.
|
|
537
|
-
|
|
538
|
-
This is what the sheets API returns.
|
|
539
|
-
|
|
540
|
-
```
|
|
541
|
-
{"condition":{"type":"DATE_EQ","values":[{"relativeDate":"TODAY"}]}}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
Which would be translated into a criteria type of DATE_EQUAL_TO_RELATIVE in GAS, with the value SpreadsheetApp.DataValidation.Criteria.TODAY
|
|
545
|
-
|
|
546
|
-
#### datavalidation enum and relative dates
|
|
547
|
-
|
|
548
|
-
Despite being able to return a criteriaType of \_RELATIVE, these are not documented in the criteriaType ENUM (https://developers.google.com/apps-script/reference/spreadsheet/data-validation-criteria), do not have corresponding require builder functions, and although they can be set using the withCriteria method, they create an invalid dataValidation (https://issuetracker.google.com/issues/418495831).
|
|
549
|
-
|
|
550
|
-
These 3 relatives exist as keys of SpreadsheetApp.DataValidationCriteria, but none of the other DATE enum values exist
|
|
551
|
-
|
|
552
|
-
- DATE_AFTER_RELATIVE
|
|
553
|
-
- DATE_BEFORE_RELATIVE
|
|
554
|
-
- DATE_EQUAL_TO_RELATIVE
|
|
555
|
-
|
|
556
|
-
I'll implement these 3 realtives in gasFakes, but treat the others as invalid. However, you cannot set these as the sheets API doesnt support seting of relative dates with Data Validation and neither does GAS - which doesnt throw an error. I believe it should, so I'm going to throw an error if you try.
|
|
557
|
-
|
|
558
|
-
#### datavalidation with formulas
|
|
559
|
-
|
|
560
|
-
Normally there's a strict check on the input to .requirexxx methods (for example dates, numbers etc). However the Sheets UI and the Sheets API allow these values to be formulas - and the formulas are stored as the user enters them. When using GAS, you would normally use a custom formula for these occasions.
|
|
561
|
-
|
|
562
|
-
In other words - here's what happens in GAS when you retrieve a data validation that has had a formula used as its value
|
|
563
|
-
|
|
564
|
-
```
|
|
565
|
-
console.log (cb.getCriteriaType().toString()) // DATE_EQUAL_TO
|
|
566
|
-
console.log (cb.getCriteriaValues()) // [ '=I1' ]
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
and yet, you get the error 'The parameters (String) don't match the method signature for SpreadsheetApp.DataValidationBuilder.requireDate.' with this.
|
|
570
|
-
|
|
571
|
-
```
|
|
572
|
-
SpreadsheetApp.newDataValidation().requireDate("=i1")
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
Another way to bypass the argument validation is to use withCriteria. For example, this will work, even though the string argument would have been rejected by requireDate()
|
|
576
|
-
|
|
577
|
-
```
|
|
578
|
-
SpreadsheetApp.newDataValidation().withCriteria(SpreadsheetApp.DataValidationCriteria.DATE_AFTER,['=i1']).build()
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
I'm leaving these same behaviors in place, and you would need to use the same workarounds as you do in GAS.
|
|
582
|
-
|
|
583
|
-
#### mixing real dates and relative dates
|
|
584
|
-
|
|
585
|
-
Since only relative versions of single dates are implemented in GAS, there's no need to handle mixed relative and real dates. As an aside, there's no validation in the UI, so you can enter any nonsense in the from and to values.
|
|
586
|
-
|
|
587
|
-
#### sheets notes
|
|
588
|
-
|
|
589
|
-
Normally, range.setNote ("takes a string"). However it does allow a numeric argument as well, which it converts to a string. However a normal toString() - for example 25.toString() would give "25". Apps script however returns "25.0" if we use getNotes() on a range whose notes has been set with setNotes() but "25" if the note was set with setNote().
|
|
590
|
-
|
|
591
|
-
There's an issue reported here - https://issuetracker.google.com/issues/429373214 - for now I'm returing "25.0" in all cases till we see what the actual resolution of this issue should be
|
|
592
|
-
|
|
593
|
-
#### Locale of dates
|
|
594
|
-
|
|
595
|
-
CriteriaValues are stored as a string, exactly as typed by the user. This means that if the API is operating in a different locale to the sheet, date formats will be different and wrong (for example - 20/2/23 in UK is 2/20/23 in US). This is a problem you would anyway face in Apps Script so I don't plan to handle this right now.
|
|
596
|
-
|
|
597
|
-
## Various hints when using the advanced sheets service
|
|
598
|
-
|
|
599
|
-
I've tried to exactly imitate the behavior of the Sheets advanced service (even though it's often inconvenient and inconsistent), so these following comments apply to both Sheets and FakeSheets. If you are usng the Advanced service, here's a few hints Ive come across that might be helpful.
|
|
600
|
-
|
|
601
|
-
### Advanced sheets updating cells
|
|
602
|
-
|
|
603
|
-
The advanced sheets service provides a huge list of builders such as Sheets.newCellData(). This is supposed to simplify building requests using the Sheets service, rather than building the requests from scratch your self. I sometimes find them more long winded that just making the objects, and I notice that there are no checks on the values that you set using them, so there's not any validation to proft from.
|
|
604
|
-
|
|
605
|
-
In any case, I've implemented them all (note that there are some GAS bugs on some of these) - https://issuetracker.google.com/issues/423737982)
|
|
606
|
-
|
|
607
|
-
I mainly use them when emulating Apps Script SpreadsheetApp services too as a double check that they are working as intended, but sometimes I build the requests up from scratch if it makes the automation simpler.
|
|
608
|
-
|
|
609
|
-
If you want to see how these are all generated, see the constructor in services/advsheets/fakeadvsheets.
|
|
610
|
-
|
|
611
|
-
#### Handling multiple response variations and formats.
|
|
612
|
-
|
|
613
|
-
If you retrieve a cell format that has been set in the UI (or in Apps Script), you often get a less full response than one that has been set using the API. If you are using the Advanced Sheets Service, and you ask for "numberFormat" for example, you may get just the pattern (0.###) or you may get the full cellformat data { type: "NUMBER", pattern: "0.###""}. You'll have to be ready to handle either type of response depending on how (and perhaps even when) the value was originally created. This could apply to any fetches of format values.
|
|
614
|
-
|
|
615
|
-
Something like this should do the trick.
|
|
616
|
-
|
|
617
|
-
```js
|
|
618
|
-
const extractPattern = (response) => {
|
|
619
|
-
// a plain pattern entered by UI, apps script or lax api call
|
|
620
|
-
if (is.string(response)) return response;
|
|
621
|
-
// should be { type: "TYPE", pattern: "xxx"}
|
|
622
|
-
if (!is.object(response) || !Reflect.has(response, "pattern")) return null;
|
|
623
|
-
return response.pattern;
|
|
624
|
-
};
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
To emulate the regular SpreadsheetApp behavior, `fakeRange.getNumberFormat()` will strip out any extra stuff and just return the pattern. `fakeRange.setNumberFormat("0.###")` will always set the complete cellformat object { type: "NUMBER", pattern: "0.###"}
|
|
628
|
-
|
|
629
|
-
##### Numberformat default pattern
|
|
630
|
-
|
|
631
|
-
Normally we can use a null value to reset a format to the default UI value. However, number format will fail messily with a null argument. The correct way is `setNumberFormat('general')` even though `getNumberFormat()` returns '0.###############" or similar. If using Advanced Sheets, you still need to use the 'pattern' approach - { pattern: "general", type: "NUMBER" }
|
|
632
|
-
|
|
633
|
-
#### Text direction
|
|
634
|
-
|
|
635
|
-
Unlike other similar functions, `setTextDirection(TextDirection)` takes an enum argument and `getTextDirection()` returns an enum too. `setTextDirection(null)` will reset to default behavior, but a subsequent `getTextDirection()` will return null, rather than a default value. This allows the Sheets UI to make an in context decision based on language locale.
|
|
636
|
-
|
|
637
|
-
#### Horizontal alignment
|
|
638
|
-
|
|
639
|
-
The documented acceptable values to `range.setHorizontalAlignment()` are left, center, normal, null. However right is also valid so I'm supporting that too. `range.getHorizontalAlignment()` returns left,center,right,general,general-left. Although the alignment behavior for 'general' and 'general-left' in the UI appears identical, `range.setHorizontalAlignment(null)` returns 'general', whereas `range.setHorizontalAlignment('normal')` returns 'general-left'. There doesn't appear to be a way to force a 'general-left' return via the Sheets API or advanced service.
|
|
640
|
-
|
|
641
|
-
As with most of these format setting methods, Apps Script will silently ignore invalid arguments. I've generally throw an error if an invalid value argument is sent so, by design, `range.setHorizontalAlignment('foo') will throw an error on FakeGas, but not on Apps Script.
|
|
642
|
-
|
|
643
|
-
#### Wrap and Wrap strategy
|
|
644
|
-
|
|
645
|
-
Initially a cell will return OVERFLOW for `getWrapStrategy` and true for `getWrap`. This is wrong as OVERFLOW should be paired with false. Once you set wrapStrategy explicitly to OVERFLOW, it returns the correct value of false.
|
|
646
|
-
|
|
647
|
-
The Apps Script issue for that is here https://issuetracker.google.com/issues/427134600
|
|
648
|
-
|
|
649
|
-
#### range.copyValuesToRange
|
|
650
|
-
|
|
651
|
-
The documentaton for this method says - "Copy the content of the range to the given location. If the destination is larger or smaller than the source range then the source is repeated or truncated accordingly."
|
|
652
|
-
|
|
653
|
-
This implies that a smaller destination range that the source should only paste a truncated version of the source range. In fact it pastes it all - see issue https://issuetracker.google.com/issues/427192537
|
|
654
|
-
|
|
655
|
-
So in summary the current behavior of this function in Apps Script doesn't match the documentation in these ways:
|
|
656
|
-
|
|
657
|
-
- If the target range is smaller than the source range, it does not truncate, but always copies the entire range even if it violates the dimensions of the target range.
|
|
658
|
-
- If the target range is larger than the source range, it only duplicates enough times where it can fit the entire source data into what's remaining in the target range.
|
|
659
|
-
|
|
660
|
-
#### range.copyTo
|
|
661
|
-
|
|
662
|
-
The variant of copyToRange suffers from the same problems as .copyValuesToRange. But there are others too. I've decided to implement them cleanly in the hope that the issues in Apps Script will one day be fixed - see issue https://issuetracker.google.com/issues/427192537
|
|
663
|
-
|
|
664
|
-
1. I also note that range.copyTo() has the same behavior
|
|
665
|
-
|
|
666
|
-
2. the documentation for copyTo says "A destination range to copy to; only the top-left cell position is relevant." - This is not true - since duplication or truncation will happen depending on the size of the output range, just as with range.copyValuesToRange and range.copyFormatToRange().
|
|
114
|
+
### Pushing files to GAS
|
|
667
115
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
```
|
|
673
|
-
range.copyTo (destination) // valid
|
|
674
|
-
range.copyTo (destination, SpreadsheetApp.CopyPasteType.PASTE_VALUES) // valid
|
|
675
|
-
range.copyTo (destination, "FOO", true) // correctly reports FOO as invalid type
|
|
676
|
-
range.copyTo (destination, "FOO") // ignores FOO and runs without reporting error
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
5. There is no conflicting option error thrown for
|
|
680
|
-
|
|
681
|
-
```
|
|
682
|
-
range.copyTo(destination, { contentsOnly: true, formatOnly: true }) // should throw error for conflicting options
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
6. Does not check for invalid options
|
|
686
|
-
|
|
687
|
-
```
|
|
688
|
-
range.copyTo(targetRange, { foo: true }) // should throw an error for invalid option
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
7. Paste values can also reset paste formats (doesnt happen with advanced sheets)
|
|
692
|
-
|
|
693
|
-
```
|
|
694
|
-
range.copyTo (destination, SpreadsheetApp.CopyPasteType.PASTE_VALUES) /// this can also trash formats previously set with PASTE_FORMAT
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
#### checking of invalid arguments
|
|
698
|
-
|
|
699
|
-
In many cases, Apps Script doesn't check the validity of invalid arguments - for example (sse this issue https://issuetracker.google.com/issues/428869869), it happily accepts invalid colors.
|
|
700
|
-
|
|
701
|
-
```
|
|
702
|
-
range.setBackground("foo")
|
|
703
|
-
range.setBackground("#gggggg")
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
Generally I type check most arguments, so may throw an error when Apps Script doesn't - but I also raise as an issue with the Apps Script team in the hope they'll fix it one day. As a result you may find some t.threw() tests are skipped when running in the real Apps Script environment.
|
|
707
|
-
|
|
708
|
-
#### TextRotation
|
|
709
|
-
|
|
710
|
-
Apps Script returns a `TextRotation` object to `range.getTextRotation()`, which has both an 'isVertical()' and `getDegrees()` method. There is an overload for the `setTextRotation(degrees)` function - `setTextRotation(TextRotation)` which theoretically allows you to set a vertical or and angle. https://developers.google.com/apps-script/reference/spreadsheet/range#settextrotationrotation
|
|
711
|
-
|
|
712
|
-
However, unlike most objects like this, there is not a `SpreadsheetApp.newTextRotation()`, and the object returned by `getTextRotation()` is readonly with no set variants. Trying to pass a plain JavaScript object with the assumed properties results in this error.
|
|
713
|
-
|
|
714
|
-
```
|
|
715
|
-
Exception: The parameters ((class)) don't match the method signature for SpreadsheetApp.Range.setTextRotation.
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
So the conclusion is that the overload for `setTextRotation(TextRotation)` does not work, so I won't be implementing this until the issue is resolved. `setTextRotation(degrees)` has been implemented of course.
|
|
719
|
-
|
|
720
|
-
[See this issue for more information ](https://issuetracker.google.com/issues/425390984)
|
|
721
|
-
|
|
722
|
-
Here's Gemini's verdict on textRotation
|
|
723
|
-
|
|
724
|
-
"You are absolutely right, and I sincerely apologize once again for the continuous string of incorrect information regarding SpreadsheetApp's TextRotation capabilities. This specific part of the Apps Script API is surprisingly complex and poorly documented/intuitive."
|
|
725
|
-
|
|
726
|
-
There's also a bug in the advanced sheet service - it doesn't return an angle in its response, even though it is set in the UI and even though Range.getTextRotation() correctly returns the angle. See https://issuetracker.google.com/issues/425390984.
|
|
727
|
-
|
|
728
|
-
Since I'm using the API I can't detect the angle until that issue is fixed, so an angle set by the UI will always be seen as 0.
|
|
729
|
-
|
|
730
|
-
#### Dates and sheets advanced service
|
|
731
|
-
|
|
732
|
-
Dates can be stored in 'Excel dateserial' format in the API. This is a float showing how many days have passed since the Excel epoch which was Dec 30th, 1899. Here's a function to convert JS dates to that, which may be helpful if you are using the sheets advanced service, rather than the SpreadsheetApp service.
|
|
733
|
-
|
|
734
|
-
```js
|
|
735
|
-
const dateToSerial = (date) => {
|
|
736
|
-
if (!is.date(date)) {
|
|
737
|
-
throw new Error(`dateToSerial is expecting a date but got ${is(date)}`);
|
|
738
|
-
}
|
|
739
|
-
// these are held in a serial number like in Excel, rather than JavaScript epoch
|
|
740
|
-
// so the epoch is actually Dec 30 1899 rather than Jan 1 1970
|
|
741
|
-
const epochCorrection = 2209161600000;
|
|
742
|
-
const msPerDay = 24 * 60 * 60 * 1000;
|
|
743
|
-
const adjustedMs = date.getTime() + epochCorrection;
|
|
744
|
-
return adjustedMs / msPerDay;
|
|
745
|
-
};
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
To enter this, you submit do this to create the value for your updateCells request body.
|
|
749
|
-
|
|
750
|
-
```js
|
|
751
|
-
const value = Sheets.newExtendedValue().setNumberValue(dateToSerial(value));
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
Note that this simply enters the numeric value of the dateSerial, without mentioning that it actually a date. To fix it as a date, you'll need to follow up with an userEnterFormat request to set the type to a date along with a custom format if required.
|
|
755
|
-
|
|
756
|
-
#### UI settings
|
|
757
|
-
|
|
758
|
-
Some of the options available in the GAS UI for setting or examining data validation are not available via GAS, and may not be available via Sheets. I'll update that later once I've figured the exact omissions and dicovered if there's a workaround. Since I'm implementing what GAS can currently do, not what it should do, this may not be an issue - just disappointing omissions.
|
|
759
|
-
|
|
760
|
-
##### examples of UI settings not intuitively settable in GAS service
|
|
761
|
-
|
|
762
|
-
- allow multiple selections - needs the allowMultipleSelections set to true - you need to you advanced service to set this
|
|
763
|
-
- display style - chip - This needs the displayStyle property set to "CHIP" - you need to you advanced service to set this
|
|
764
|
-
- color for drop downs - haven't looked into this, but it's not possible via regular gas service.
|
|
765
|
-
|
|
766
|
-
###### showCustomUI
|
|
767
|
-
|
|
768
|
-
This API property controls whether to show a drop down as plain text, or to use a fancy display such as chip or arrow. In the UI the default is true, and the displayStyle is "CHIP". As mentioned though you can't set the displayStyle with SpreadsheetApp, so setting showCustomUI true via the datavalidation builder will give you the arrow displayStyle.
|
|
769
|
-
|
|
770
|
-
In the Apps Script DataValidation builder, setting showCustomUi is achieved via the boolean 2nd argument(known as showDropdown) to requireValueInList() and requireValueInRange().
|
|
771
|
-
|
|
772
|
-
Despite the various defaults, a missing value for these properties returned via the Sheets API always means false, and a missing displayStyle with showCustomUi set to true default is "ARROW".
|
|
773
|
-
|
|
774
|
-
### Document API
|
|
775
|
-
|
|
776
|
-
Getting started on the advanced services of the Document API. These notes are for my TIL (things I learned today), but may be useful if you are digging into the Document API yourself.
|
|
777
|
-
|
|
778
|
-
#### Tabs
|
|
779
|
-
|
|
780
|
-
Tabs are a recent addition to Docs, and have added a bit of complication to handling Document responses. Here's what they say happens.
|
|
781
|
-
|
|
782
|
-
```
|
|
783
|
-
[docs](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/get)
|
|
784
|
-
|
|
785
|
-
suggestionsViewMode - enum (SuggestionsViewMode)
|
|
786
|
-
|
|
787
|
-
The suggestions view mode to apply to the document. This allows viewing the document with all suggestions inline, accepted or rejected. If one is not specified, DEFAULT_FOR_CURRENT_ACCESS is used.
|
|
788
|
-
|
|
789
|
-
includeTabsContent - boolean
|
|
790
|
-
|
|
791
|
-
Whether to populate the Document.tabs field instead of the text content fields like body and documentStyle on Document.
|
|
792
|
-
|
|
793
|
-
When True: Document content populates in the Document.tabs field instead of the text content fields in Document.
|
|
794
|
-
|
|
795
|
-
When False: The content of the document's first tab populates the content fields in Document excluding Document.tabs. If a document has only one tab, then that tab is used to populate the document content. Document.tabs will be empty.
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
It's actually a little more complicated than that - here are the properties of the each response variation.
|
|
799
|
-
|
|
800
|
-
##### case 1 {includeTabsContent: false}
|
|
801
|
-
|
|
802
|
-
{ r1k:
|
|
803
|
-
|
|
804
|
-
r2k:
|
|
805
|
-
[ 'suggestionsViewMode',
|
|
806
|
-
'documentId',
|
|
807
|
-
'tabs',
|
|
808
|
-
'title',
|
|
809
|
-
'revisionId' ],
|
|
810
|
-
r3k:
|
|
811
|
-
[ 'title',
|
|
812
|
-
'body',
|
|
813
|
-
'namedStyles',
|
|
814
|
-
'revisionId',
|
|
815
|
-
'documentId',
|
|
816
|
-
'suggestionsViewMode',
|
|
817
|
-
'documentStyle' ] }
|
|
818
|
-
14:09:40 Info { r1b: [ 'content' ],
|
|
819
|
-
r2b: [ 'documentTab', 'tabProperties' ],
|
|
820
|
-
r3b: [ 'content' ],
|
|
821
|
-
r2tb: [ 'documentStyle', 'body', 'namedStyles' ] }
|
|
822
|
-
|
|
823
|
-
Response has these properties:
|
|
824
|
-
|
|
825
|
-
```
|
|
826
|
-
'revisionId',
|
|
827
|
-
'documentStyle',
|
|
828
|
-
'body',
|
|
829
|
-
'title',
|
|
830
|
-
'suggestionsViewMode',
|
|
831
|
-
'documentId',
|
|
832
|
-
'namedStyles'
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
The body contains just 1 property - `content`
|
|
836
|
-
|
|
837
|
-
##### case 2 {includeTabsContent: true}
|
|
838
|
-
|
|
839
|
-
Response has these properties:
|
|
840
|
-
|
|
841
|
-
```
|
|
842
|
-
'suggestionsViewMode',
|
|
843
|
-
'documentId',
|
|
844
|
-
'tabs',
|
|
845
|
-
'title',
|
|
846
|
-
'revisionId'
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
The tabs property is an array of tabs, the first of which contains these properties
|
|
850
|
-
|
|
851
|
-
```
|
|
852
|
-
documentTab, tabProperties
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
The documentTab has these properties - so the tab[0] in a document with no tabs isn't exactly the same as the legacy style as implied in the docs, since the document metadata is not repeated in each tab.
|
|
856
|
-
````
|
|
857
|
-
'documentStyle', 'body', 'namedStyles'
|
|
116
|
+
There are a couple of syntactical differences between Node and Apps Script. Not in the body of the code but in how the IDE executes. The 2 main ones are
|
|
117
|
+
- apps script doesnt support 'import'. Alls its top level variables are global, so we need to drop imports from the files that are pushed to the IDE
|
|
118
|
+
- Script run on Node are called immediately. Normally on Apps Script we hit the run button. Here's how I handle this in my scripts that need to run on both environments.
|
|
858
119
|
````
|
|
859
|
-
|
|
120
|
+
// this required on Node but not on Apps Script
|
|
121
|
+
if (ScriptApp.isFake) testFakes()
|
|
860
122
|
````
|
|
861
|
-
|
|
862
|
-
````
|
|
863
|
-
|
|
864
|
-
##### case 3 - default
|
|
865
|
-
|
|
866
|
-
Response includes thes same keys as case 1. As an aside, the property orders are all unpredictable so you can't just compare stringified versions of the response.
|
|
867
|
-
|
|
868
|
-
```
|
|
869
|
-
'title',
|
|
870
|
-
'body',
|
|
871
|
-
'namedStyles',
|
|
872
|
-
'revisionId',
|
|
873
|
-
'documentId',
|
|
874
|
-
'suggestionsViewMode',
|
|
875
|
-
'documentStyle'
|
|
876
|
-
```
|
|
877
|
-
|
|
878
|
-
### Enums
|
|
879
|
-
|
|
880
|
-
All Apps Script enums are imitated using a seperate class 'newFakeGasenum()'. A complete write up of that is in [fakegasenum](https://github.com/brucemcpherson/fakegasenum). The same functionality is also available as an Apps Script library if you'd like to make your own enums over on GAS just like you find in Apps Script.
|
|
881
|
-
|
|
882
|
-
### Auth
|
|
883
|
-
|
|
884
|
-
Sometime between v144 and v150 of googleapis library, it appeared to become mandatory to include the project id in the auth pattern for API clients. Since we get the project id from the ADC, we actually have to do double auths. One to get the project id (which is async), and another to get an auth with the scopes required for the sheets, drive etc client (which is not async). All this now taken care of during the init phase, so look at an existing getauthenticated client function for how if you are adding a new service,
|
|
885
|
-
|
|
886
|
-
## Some experiences with using Gemini code assist
|
|
887
|
-
|
|
888
|
-
I tried using Gemini to generate the code and test cases for a number of method types. The results were mixed ranging from 'wow, how did it do that' to endless hallucinatory loops with Gemini insisting it was right despite the evidence. In the end I think it was mildly helpful but probably didnt save me any time or effort. It was just a different kind of effort.
|
|
889
|
-
|
|
890
|
-
Another annoyance is after deep sessions of back and forwards, code assist is generally unable to make the changes automatically and often reverts to an empty gray sidebar - which means you have to start again. Recalling the history doesn't necessarily reinstate where you were.
|
|
891
|
-
|
|
892
|
-
I also dislike the habit gemini has of 'mansplaining' back to me the answer I've just provided to correct some of it's code.
|
|
893
|
-
|
|
894
|
-
#### range.banding
|
|
895
|
-
|
|
896
|
-
This was a fairly convoluted section. I used gemini code assist heavily on this to do the legwork and all in all it made a pretty decent job of it, although with the endlessly repeated updates and test refactoring it took longer from start to finish than I would have expected it to take had I done it from scratch manually as all the previous classes. I think the right approach going forward is mainly manual with gemini doing the busy work. The tests Gemini came up with were also far from exhaustive, and pretty much ignored edge cases, so it needed additional requests to add more robust tests. On the plus side, it very quickly figured out how to reuse functions that already existed.
|
|
897
|
-
|
|
898
|
-
#### developer meta data
|
|
899
|
-
|
|
900
|
-
As per range.banding, I initially used Gemini to create much of the methods and tests associated with this. This was tortuous with Gemini going round in circles making the same mistakes over and over, eventually crashing and having to start again. After an entire day, I picked it up manually - which I should have done much earlier.Since I did not create the developer data methods in the first place, it's very hard to pick up and debug where Gemini left off as it's repeated attempts left behind some very convoluted code. A learning here is that if it looks like Gemini is flailing and failing, take over early.
|
|
123
|
+
For inspiration on pushing modified files to the IDE, see the [bash script](https://github.com/brucemcpherson/gas-fakes/blob/main/togas.bash) I use for the test suite.
|
|
901
124
|
|
|
902
|
-
As an aside, I find the implementation of developer meta data very messy and inconsistent with the usual Apps Script services. I believe that regular Apps Script developers will find it unfamiliar, restrictive and intimidating (which is maybe why it never really caught on)
|
|
903
|
-
|
|
904
|
-
#### grouping and collapsing
|
|
905
|
-
|
|
906
|
-
Gemini took me down a rabbit hole on this one, where it kept forgetting that the objective was to have the fake environment behave as Apps Script. Up to this point, Gemini was quite good at remembering this, but for some reason for this collection of methods it kept fiddling with the test cases to make them work differently in each environment rather than replicating the Apps Script behavior and having the same tests pass in both environments.
|
|
907
|
-
|
|
908
|
-
In particular it started to believe that the Apps Script environment was not atomic and to try to modify tests with Utilities.sleep everywhere and many other false avenues.
|
|
909
|
-
|
|
910
|
-
Quite often all that is needed is for you to read the documentation yourself, undo the unnecessary labyrinth of gemini changes, and paste a copy of the documentation into the gemini context to get it back on track. A lesson to take from this is to start the emulation task by providing the more complex parts of the documentaion instead of relying on gemini to look them up.
|
|
911
|
-
|
|
912
|
-
#### pivot tables
|
|
913
|
-
|
|
914
|
-
There are many classes and methods required to support pivot table, so I decided to try to have gemini build them all. I found that building a placeholder class at a time, adding checking and correcting methods as we go, was the best approach. By this time Gemini was intuitively building classes that looked the same as the others, using the same shared helper functions and approaches.
|
|
915
|
-
|
|
916
|
-
Gemini tends to have its own opinion about which methods should exist in apps script classes and this is almost always wrong. I found it best to supply the list of methods that should exist along with a link to the documenation for best results. There are some undocumented methods in some Apps Script classes, so as a final check I often review the Object.keys() of an instance to see if any are missing from the documentation.
|
|
917
|
-
|
|
918
|
-
Despite this Gemini will often create unknown methods, miss known ones, and attempt to reference private functions and methods when creating test cases. There were also quite a few occurrences of gemini introducing bugs in to previously tested material, so I found I needed to re-run not only the tests I was working on, but also other vaguely realated ones too.
|
|
919
|
-
|
|
920
|
-
In summary, Gemini has achieved a lot of good work with this collection of classes, however I don't yet feel completetly confident that we have a completely robust set of implementations. I think I have to write some more edge case testing manually to properly excercise this. The tests created by Gemini are relatively superficial.
|
|
921
|
-
|
|
922
|
-
On the other hand, using these techniques meant that we got the entire pivot table collection of classes and tests to this point in about a day, mainly tracking down filter hallucinations. It would probably have taken me a few days to do it all manually.
|
|
923
|
-
|
|
924
|
-
As a general rule, once Gemini starts talking about making sweeping changes and suggesting that the Sheets API has bugs that is causing everything to collapse, it's time to start a new session. We did go down this rabbit hole a few times when working on pivot table filter criteria and as a result we've ended up with a lot of messy, hard to understand and unplesant code in these classes, following many attempts by gemini to diagnose self inflicted issues. I may have to back and take out some of the redundancy at some point.
|
|
925
|
-
|
|
926
|
-
#### Datasource
|
|
927
|
-
|
|
928
|
-
These have been basicly implemented, but remain untested in any way. I haven't been able to test and refine these as I don't have the right level of an expensive enough workspace license. Will come back to that at a later date - TODO.
|
|
929
|
-
|
|
930
|
-
#### r1c1 style ranges
|
|
931
|
-
|
|
932
|
-
The Sheets API doesn't know about these, so all r1c1 style methods such as setFormulasR1C1 include a conversion to a regular range to be able to communicate with the underlying sheets API. This can get pretty complex, so we have rudimentary, mainly Gemini generated functions to handle that.
|
|
933
|
-
|
|
934
|
-
#### Intial verdict on using Gemini to generate some of this stuff
|
|
935
|
-
|
|
936
|
-
I'm torn. On the one hand, it's been great at doing busy work like writing test cases and detecting dependencies that I might otherwise have missed. It can often be pretty good at refactoring/renaming things. On the other hand, if it gets it wrong, it's very hard to get it back on track as it tries bury itself deeper and deeper into previous misconceptions. It also has huge difficulty in updating large files no matter the detailed guidance. The usual end game is to restart a fresh context and/or copy and paste the content into a file you create manually.
|
|
937
|
-
|
|
938
|
-
There were ocassions when the content Gemini provided content to be copied and pasted that was invalid syntax, or worse, dropped lines of code in sections it didn't plan to make any changes. In particular, code that had something like `ob[method](args)` was regularily truncated to just `obmethod`. I've found that if you enter `ob[method](args)` in the code assist chat window it will also interpret it as `obmethod` unless you escape the brackets (which of course you wouldnt do in code).
|
|
939
|
-
|
|
940
|
-
Another issue is that Gemini can take 10 mins or more to create the full content for a large class in its chat window, sometimes ending in the gray screen of death. I've found it's best to completely avoid using Gemini to make minor changes, but to just make them manually.
|
|
941
|
-
|
|
942
|
-
Overall it saves some time, for sure. However, the result is often suboptimal, wordy, lacking in reusability and not something I would be be happy to put my name to. From a coder perspective, the role becomes one of repetetive specification, debugging, checking and testing, while failing to develop a deep understanding of the work in hand. I like coding, so from a satisfaction perspective, I'm not entirely convinced yet. I've found it's very impressive when creating small, standalone scripts but deteriorates rapidly both in speed and effectiveness as the codebase and dependencies grows. There's a point at which it becomes more trouble than it's worth.
|
|
943
|
-
|
|
944
|
-
## Testing
|
|
945
|
-
|
|
946
|
-
I recommend you use the test project included in the repo if you want to do some tests. It uses a Fake DriveApp service to excercise Auth etc. Just change the fixtures in your own environments by following the instructions in [setup-env.md](https://github.com/brucemcpherson/gas-fakes/blob/main/setup-env.MD), then `npm i && npm test`.
|
|
947
|
-
|
|
948
|
-
It's not necessary to run these tests, but if you are a collaborator and want to add some additional methods, you need to create a test section and always run a full set of tests before creating a merge request to ensure your changes havent broken anything. At the time of writing there are about 4,000 active tests.
|
|
949
|
-
|
|
950
|
-
Note that I use a [unit tester](https://ramblings.mcpher.com/apps-script-test-runner-library-ported-to-node/) that runs in both GAS and Node, so the exact same tests will run in both environments. There are some example tests in the repo. Each test has been proved on both Node and GAS. There's also a shell (togas.sh) which will use clasp to push the test code to Apps Script.
|
|
951
|
-
|
|
952
|
-
Each test can be run individually (for example `npm run testdrive`) or all with `npm test`
|
|
953
|
-
|
|
954
|
-
Test settings and fixtures are in the .env file. Some readonly files are publicly shared and can be left with the example value in .env-template. Most files which are written are created and deleted afterwards on successful completion. They will be named something starting with --. In case of failures you may need to delete these yourself. If you want to preserve the testfiles it creates doing a test session, just set the CLEAN parameter in .env to 0.
|
|
955
|
-
|
|
956
|
-
### Pushing to GAS
|
|
957
|
-
|
|
958
|
-
The script uses togas.sh will move the test scripts to GAS with clasp - just set the `SOURCE` and `TARGET` folders in the TOGAS script. Make sure you have an `appsscript.json` manifest in the `SOURCE` folder, as **gas-fakes** reads that to handle OAuth on Node.
|
|
959
|
-
|
|
960
|
-
You can write your project to run on Node and call GAS services, and it will also run on the GAS environment with no code changes, except on the Node side you have this one import
|
|
961
|
-
|
|
962
|
-
```sh
|
|
963
|
-
// all the fake services are here
|
|
964
|
-
import '@mcpher/gas-fakes/main.js'
|
|
965
|
-
```
|
|
966
125
|
|
|
967
|
-
togas.sh will remove imports and exports on the way to apps script, which doesnt support them.
|
|
968
126
|
|
|
969
127
|
## Help
|
|
970
128
|
|
|
@@ -972,5 +130,17 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
972
130
|
|
|
973
131
|
## Translations and writeups
|
|
974
132
|
|
|
975
|
-
- [
|
|
133
|
+
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
134
|
+
- [Inside the volatile world of a Google Document](https://ramblings.mcpher.com/inside-the-volatile-world-of-a-google-document/
|
|
135
|
+
- [Apps Script Services on Node – using apps script libraries](https://ramblings.mcpher.com/apps-script-services-on-node-using-apps-script-libraries/)
|
|
136
|
+
- [Apps Script environment on Node – more services](https://ramblings.mcpher.com/apps-script-environment-on-node-more-services/)
|
|
137
|
+
- [Turning async into synch on Node using workers](https://ramblings.mcpher.com/turning-async-into-synch-on-node-using-workers/)
|
|
138
|
+
- [All about Apps Script Enums and how to fake them](https://ramblings.mcpher.com/all-about-apps-script-enums-and-how-to-fake-them/)
|
|
976
139
|
- [Russian version](README.RU.md) ([credit Alex Ivanov](https://github.com/oshliaer)) - needs updating
|
|
140
|
+
- [colaborators](collaborators.md) - additional information for collaborators
|
|
141
|
+
- [oddities](oddities.md) - a collection of oddities uncovered during this project
|
|
142
|
+
- [gemini](gemini.md) - some reflections and experiences on using gemini to help code large projects
|
|
143
|
+
- [named colors](named-colors.md) - colors supported by Apps Script
|
|
144
|
+
- [setup env](setup-env.md) - ([credit Eric Shapiro] - additional info on contents of .env file
|
|
145
|
+
- [this file](README.md)
|
|
146
|
+
- [named colors](named-colors.md)
|