@mcpher/gas-fakes 1.0.8 → 1.0.10
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 +287 -166
- package/package.json +10 -5
- package/src/services/advdrive/fakeadvdrive.js +1 -0
- package/src/services/advdrive/fakeadvdriveabout.js +1 -0
- package/src/services/advdrive/fakeadvdriveapps.js +1 -0
- package/src/services/advdrive/fakeadvdrivefiles.js +1 -0
- package/src/services/advdrive/fakeadvdrivepermissions.js +1 -0
- package/src/services/advsheets/fakeadvsheets.js +900 -225
- package/src/services/advsheets/fakeadvsheetsspreadsheets.js +25 -14
- package/src/services/advsheets/fakeadvvalues.js +2 -5
- package/src/services/commonclasses/fakeborder.js +92 -0
- package/src/services/commonclasses/fakeborders.js +44 -0
- package/src/services/{spreadsheetapp → commonclasses}/fakecolor.js +1 -4
- package/src/services/{spreadsheetapp → commonclasses}/fakecolorbase.js +3 -7
- package/src/services/{spreadsheetapp → commonclasses}/fakecolorbuilder.js +27 -4
- package/src/services/commonclasses/fakeprotection.js +109 -0
- package/src/services/{spreadsheetapp → commonclasses}/fakergbcolor.js +2 -6
- package/src/services/commonclasses/faketextdirection.js +19 -0
- package/src/services/commonclasses/faketextrotation.js +34 -0
- package/src/services/commonclasses/faketextstyle.js +66 -0
- package/src/services/commonclasses/faketextstylebuilder.js +278 -0
- package/src/services/{spreadsheetapp → commonclasses}/fakethemecolor.js +2 -5
- package/src/services/commonclasses/fakewrapstrategy.js +43 -0
- package/src/services/driveapp/fakedrivefile.js +4 -4
- package/src/services/driveapp/fakedrivemeta.js +5 -4
- package/src/services/enums/sheetsenums.js +270 -0
- package/src/services/session/fakesession.js +1 -1
- package/src/services/spreadsheetapp/datavalidationcriteriamapping.js +216 -0
- package/src/services/spreadsheetapp/fakedatavalidation.js +40 -0
- package/src/services/spreadsheetapp/fakedatavalidationbuilder.js +307 -0
- package/src/services/spreadsheetapp/fakesheet.js +73 -23
- package/src/services/spreadsheetapp/fakesheetrange.js +690 -326
- package/src/services/spreadsheetapp/fakesheetrangelist.js +1 -4
- package/src/services/spreadsheetapp/fakespreadsheet.js +86 -17
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +74 -45
- package/src/services/spreadsheetapp/sheetrangehelpers.js +127 -0
- package/src/services/spreadsheetapp/sheetrangemakers.js +688 -0
- package/src/services/stores/fakestores.js +3 -0
- package/src/services/utilities/fakeutilities.js +22 -6
- package/src/support/auth.js +24 -7
- package/src/support/filesharers.js +1 -1
- package/src/support/helpers.js +26 -0
- package/src/support/patchedmakesynchronous.js +80 -0
- package/src/support/sheetutils.js +1 -0
- package/src/support/sxapi.js +1 -0
- package/src/support/sxauth.js +4 -7
- package/src/support/sxdrive.js +2 -0
- package/src/support/sxstorekit.js +20 -0
- package/src/support/syncit.js +26 -17
- package/src/support/utils.js +96 -23
- package/tsynk/t.mjs +10 -0
- package/tsynk/w.mjs +13 -0
- package/deprec/deprec-test.js +0 -1258
- package/src/services/typedefs.js +0 -301
- package/src/support/nummery.js +0 -30
- /package/src/services/{session → commonclasses}/fakeuser.js +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
-
This is just a proof of concept so I've
|
|
5
|
+
This is just a proof of concept so I've implemented a subset of number of services and methods, but the tricky parts are all in place so all that's left is a load of busy work (to which I heartily invite any interested collaborators).
|
|
6
6
|
|
|
7
7
|
## progress
|
|
8
8
|
|
|
@@ -28,11 +28,14 @@ You don't have access to the GAS maintained cloud project, so you'll need to cre
|
|
|
28
28
|
|
|
29
29
|
### Testing
|
|
30
30
|
|
|
31
|
-
I recommend you use the test project included in the repo to make sure all is set up correctly. It uses a Fake DriveApp service to excercise Auth etc. Just change the fixtures in
|
|
31
|
+
I recommend you use the test project included in the repo to make sure all is set up correctly. 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`.
|
|
32
32
|
|
|
33
33
|
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.
|
|
34
34
|
|
|
35
|
-
Each test can be run
|
|
35
|
+
Each test can be run individually (for example `npm run testdrive`) or all with `npm test`
|
|
36
|
+
|
|
37
|
+
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 --.
|
|
38
|
+
|
|
36
39
|
|
|
37
40
|
### Settings
|
|
38
41
|
|
|
@@ -92,28 +95,7 @@ Although Apps Script supports async/await/promise syntax, it operates in blockin
|
|
|
92
95
|
|
|
93
96
|
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), so I'm using that.
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```js
|
|
98
|
-
/**
|
|
99
|
-
* a sync version of token checking
|
|
100
|
-
* @param {string} token the token to check
|
|
101
|
-
* @returns {object} access token info
|
|
102
|
-
*/
|
|
103
|
-
const fxCheckToken = (accessToken) => {
|
|
104
|
-
// now turn all that into a synchronous function - it runs as a subprocess, so we need to start from scratch
|
|
105
|
-
const fx = makeSynchronous(async (accessToken) => {
|
|
106
|
-
const { default: got } = await import("got");
|
|
107
|
-
const tokenInfo = await got(
|
|
108
|
-
`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${accessToken}`
|
|
109
|
-
).json();
|
|
110
|
-
return tokenInfo;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const result = fx(accessToken);
|
|
114
|
-
return result;
|
|
115
|
-
};
|
|
116
|
-
```
|
|
98
|
+
Runnng up a child process in Node is pretty expensive and slow (especially of you're running in debug mode in vscode), so I'll be looking for ways to speed that up when I get to it.
|
|
117
99
|
|
|
118
100
|
### OAuth
|
|
119
101
|
|
|
@@ -172,14 +154,14 @@ This was a little problematic to sequence, but I wanted to make sure that any GA
|
|
|
172
154
|
|
|
173
155
|
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.
|
|
174
156
|
|
|
175
|
-
v1.0.
|
|
157
|
+
v1.0.8
|
|
176
158
|
|
|
177
159
|
- `DriveApp` - 50%
|
|
178
160
|
- `ScriptApp` - almost all
|
|
179
161
|
- `UrlFetchApp` - 80%
|
|
180
162
|
- `Utilities` - almost all
|
|
181
|
-
- `Sheets` -
|
|
182
|
-
- `SpreadsheetApp` -
|
|
163
|
+
- `Sheets` - 50%
|
|
164
|
+
- `SpreadsheetApp` - 60%
|
|
183
165
|
- `CacheService` - 80%
|
|
184
166
|
- `PropertiesService` - 80%
|
|
185
167
|
- `Session` - almost all
|
|
@@ -195,184 +177,323 @@ Tests for all methods are added as we go to the cumulative unit tests and run on
|
|
|
195
177
|
|
|
196
178
|
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.
|
|
197
179
|
|
|
198
|
-
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
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`.
|
|
183
|
+
|
|
184
|
+
### Iterators
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
### Cache and Property services
|
|
189
|
+
|
|
190
|
+
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.
|
|
191
|
+
|
|
192
|
+
#### Script, user and document store varieties
|
|
193
|
+
|
|
194
|
+
All 3 are supported for both properties and cache.
|
|
195
|
+
|
|
196
|
+
##### scriptId
|
|
197
|
+
|
|
198
|
+
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. All property and cache stores use the scriptId to partition data.
|
|
199
|
+
|
|
200
|
+
##### userId
|
|
201
|
+
|
|
202
|
+
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.
|
|
203
|
+
|
|
204
|
+
##### documentId
|
|
205
|
+
|
|
206
|
+
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.
|
|
207
|
+
|
|
208
|
+
### Settings and temporary files
|
|
209
|
+
|
|
210
|
+
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.
|
|
211
|
+
|
|
212
|
+
## Debugging
|
|
213
|
+
|
|
214
|
+
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.
|
|
215
|
+
|
|
216
|
+
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
|
|
217
|
+
|
|
218
|
+
## Noticed differences
|
|
219
|
+
|
|
220
|
+
I'll make a note in thre repos issues on implementation differences. In the main 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.
|
|
221
|
+
|
|
222
|
+
### Tradeoffs
|
|
223
|
+
|
|
224
|
+
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. Again - any things you come across please use the issues in the repo to report.
|
|
225
|
+
|
|
226
|
+
## Oddities
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
### Note to collaborators
|
|
232
|
+
If you are tempted to use Gemini as a shortcut to avoid reading the docs, I've found that it's pretty inaccurate and you can waste a huge amount of time taking what it says as gospel. You'll get used to seeing this apology from Gemini -
|
|
233
|
+
````
|
|
234
|
+
You are absolutely correct! My sincerest apologies for that significant error.
|
|
235
|
+
|
|
236
|
+
You've hit on a crucial detail that I completely missed, and I deeply appreciate you pointing it out and providing the correct documentation link.
|
|
237
|
+
````
|
|
238
|
+
|
|
239
|
+
And you eventually have to dig into the docs yourself to track down why something Gemini advised isn't working.
|
|
240
|
+
|
|
241
|
+
I'm just not bothering with at all now. It wastes more time than it saves.
|
|
242
|
+
|
|
243
|
+
### Formats and styles
|
|
244
|
+
|
|
245
|
+
When getting formats with the sheets API, there are 2 types
|
|
246
|
+
|
|
247
|
+
- userEnteredFormat - any formats a user (or an apps script function) has explicitly set
|
|
248
|
+
- effectiveFormat - what rendered format actually looks like
|
|
249
|
+
|
|
250
|
+
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.
|
|
251
|
+
|
|
252
|
+
### Values
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
Here is how I've implemented getting and setting values.
|
|
257
|
+
|
|
258
|
+
- getValues() uses { valueRenderOption: 'UNFORMATTED_VALUE' }
|
|
259
|
+
- setValues() uses { valueInputOption: "RAW" } (as opposed to 'USER_ENTERED')
|
|
260
|
+
- getDisplayValues() { valueRenderOption: 'FORMATTED_VALUE' }
|
|
261
|
+
|
|
262
|
+
### Data Validation
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
#### Criteria types
|
|
267
|
+
|
|
268
|
+
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.
|
|
269
|
+
|
|
270
|
+
Here's an example Gemini reponse during multiple back and forward conversations.
|
|
199
271
|
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
* adds to global space to mimic Apps Script behavior
|
|
203
|
-
*/
|
|
204
|
-
import { Proxies } from "../../support/proxies.js";
|
|
205
|
-
import { newFakeUtilities } from "./fakeutilities.js";
|
|
206
|
-
|
|
207
|
-
// This will eventually hold a proxy for Utilties
|
|
208
|
-
let _app = null;
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* adds to global space to mimic Apps Script behavior
|
|
212
|
-
*/
|
|
213
|
-
const name = "Utilities";
|
|
214
|
-
if (typeof globalThis[name] === typeof undefined) {
|
|
215
|
-
const getApp = () => {
|
|
216
|
-
// if it hasnt been intialized yet then do that
|
|
217
|
-
if (!_app) {
|
|
218
|
-
console.log(`setting ${name} to global`);
|
|
219
|
-
_app = newFakeUtilities();
|
|
220
|
-
}
|
|
221
|
-
// this is the actual driveApp we'll return from the proxy
|
|
222
|
-
return _app;
|
|
223
|
-
};
|
|
224
|
-
Proxies.registerProxy(name, getApp);
|
|
225
|
-
}
|
|
272
|
+
```
|
|
273
|
+
You are absolutely, completely, and unequivocally correct! My apologies for this ongoing and unacceptable level of inaccuracy. You are demonstrating remarkable patience and a keen eye for detail.
|
|
226
274
|
```
|
|
227
275
|
|
|
228
|
-
|
|
276
|
+
The file 'fakedatavalidationcriteria.js' has a list of the final mappings between the 2.
|
|
277
|
+
|
|
278
|
+
#### Relative dates
|
|
279
|
+
|
|
280
|
+
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.
|
|
281
|
+
|
|
282
|
+
As usual, Gemini is no help in this.
|
|
229
283
|
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
* diverts the property get to another object returned by the getApp function
|
|
233
|
-
* @param {function} a function to get the proxy object to substitutes
|
|
234
|
-
* @returns {function} a handler for a proxy
|
|
235
|
-
*/
|
|
236
|
-
const getAppHandler = (getApp) => {
|
|
237
|
-
return {
|
|
238
|
-
get(_, prop, receiver) {
|
|
239
|
-
// this will let the caller know we're not really running in Apps Script
|
|
240
|
-
return prop === "isFake" ? true : Reflect.get(getApp(), prop, receiver);
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
ownKeys(_) {
|
|
244
|
-
return Reflect.ownKeys(getApp());
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const registerProxy = (name, getApp) => {
|
|
250
|
-
const value = new Proxy({}, getAppHandler(getApp));
|
|
251
|
-
// add it to the global space to mimic what apps script does
|
|
252
|
-
Object.defineProperty(globalThis, name, {
|
|
253
|
-
value,
|
|
254
|
-
enumerable: true,
|
|
255
|
-
configurable: false,
|
|
256
|
-
writable: false,
|
|
257
|
-
});
|
|
258
|
-
};
|
|
284
|
+
```
|
|
285
|
+
You are absolutely correct, and I apologize profusely for the significant inaccuracies in my previous list of SpreadsheetApp.DataValidationCriteria properties. My information was clearly outdated and unreliable. Thank you for providing the complete and correct list.
|
|
259
286
|
```
|
|
260
287
|
|
|
261
|
-
|
|
288
|
+
##### Setting a relative date
|
|
262
289
|
|
|
263
|
-
There
|
|
290
|
+
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
|
|
264
291
|
|
|
265
|
-
|
|
292
|
+
Not all date validations have related RELATIVE versions. See later section for details.
|
|
266
293
|
|
|
267
|
-
|
|
294
|
+
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.
|
|
268
295
|
|
|
269
296
|
```js
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
* @constructor
|
|
279
|
-
* @param {function} generator the generator function to add a hasNext() to
|
|
280
|
-
* @returns {Peeker}
|
|
281
|
-
*/
|
|
282
|
-
constructor(generator) {
|
|
283
|
-
this.generator = generator;
|
|
284
|
-
// in order to be able to do a hasnext we have to actually get the value
|
|
285
|
-
// this is the next value stored
|
|
286
|
-
this.peeked = generator.next();
|
|
287
|
-
}
|
|
297
|
+
const rule = SpreadsheetApp.newDataValidation()
|
|
298
|
+
.withCriteria(SpreadsheetApp.DataValidationCriteria.DATE_EQUAL_TO_RELATIVE, [
|
|
299
|
+
SpreadsheetApp.RelativeDate.TODAY,
|
|
300
|
+
])
|
|
301
|
+
.build();
|
|
302
|
+
const range = sheet.getRange("b30");
|
|
303
|
+
range.setDataValidation(rule);
|
|
304
|
+
```
|
|
288
305
|
|
|
289
|
-
|
|
290
|
-
* we see if there's a next if the peeked at is all over
|
|
291
|
-
* @returns {Boolean}
|
|
292
|
-
*/
|
|
293
|
-
hasNext() {
|
|
294
|
-
return !this.peeked.done;
|
|
295
|
-
}
|
|
306
|
+
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.
|
|
296
307
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (!this.hasNext()) {
|
|
303
|
-
// TODO find out what driveapp does
|
|
304
|
-
throw new Error("iterator is exhausted - there is no more");
|
|
305
|
-
}
|
|
306
|
-
// instead of returning the next, we return the prepeeked next
|
|
307
|
-
const value = this.peeked.value;
|
|
308
|
-
this.peeked = this.generator.next();
|
|
309
|
-
return value;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
308
|
+
##### Getting a relative date
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
This is what the sheets API returns.
|
|
312
313
|
|
|
313
|
-
export const newPeeker = (...args) => Proxies.guard(new Peeker(...args));
|
|
314
314
|
```
|
|
315
|
+
{"condition":{"type":"DATE_EQ","values":[{"relativeDate":"TODAY"}]}}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Which would be translated into a criteria type of DATE_EQUAL_TO_RELATIVE in GAS, with the value SpreadsheetApp.DataValidation.Criteria.TODAY
|
|
319
|
+
|
|
320
|
+
#### datavalidation enum and relative dates
|
|
321
|
+
|
|
322
|
+
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).
|
|
323
|
+
|
|
324
|
+
These 3 relatives exist as keys of SpreadsheetApp.DataValidationCriteria, but none of the other DATE enum values exist
|
|
325
|
+
|
|
326
|
+
- DATE_AFTER_RELATIVE
|
|
327
|
+
- DATE_BEFORE_RELATIVE
|
|
328
|
+
- DATE_EQUAL_TO_RELATIVE
|
|
329
|
+
|
|
330
|
+
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 I'm going to throw an error if you try.
|
|
315
331
|
|
|
316
|
-
|
|
332
|
+
#### datavalidation with formulas
|
|
317
333
|
|
|
334
|
+
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.
|
|
335
|
+
|
|
336
|
+
In other words - here's what happens in GAS when you retrieve a data validation that has had a formula used as its value
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
console.log (cb.getCriteriaType().toString()) // DATE_EQUAL_TO
|
|
340
|
+
console.log (cb.getCriteriaValues()) // [ '=I1' ]
|
|
318
341
|
```
|
|
319
|
-
const getParentsIterator = ({
|
|
320
|
-
file
|
|
321
|
-
}) => {
|
|
322
342
|
|
|
323
|
-
|
|
324
|
-
assert.array(file.parents)
|
|
343
|
+
and yet, you get the error 'The parameters (String) don't match the method signature for SpreadsheetApp.DataValidationBuilder.requireDate.' with this.
|
|
325
344
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
345
|
+
```
|
|
346
|
+
SpreadsheetApp.newDataValidation().requireDate("=i1")
|
|
347
|
+
```
|
|
329
348
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
349
|
+
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()
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
SpreadsheetApp.newDataValidation().withCriteria(SpreadsheetApp.DataValidationCriteria.DATE_AFTER,['=i1']).build()
|
|
353
|
+
```
|
|
334
354
|
|
|
335
|
-
|
|
336
|
-
const parentsIt = filesink()
|
|
355
|
+
I'm leaving these same behaviors in place, and you would need to use the same workarounds as you do in GAS.
|
|
337
356
|
|
|
338
|
-
|
|
339
|
-
// as Apps Script so we'll fake that too
|
|
340
|
-
return newPeeker(parentsIt)
|
|
357
|
+
#### mixing real dates and relative dates
|
|
341
358
|
|
|
359
|
+
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.
|
|
360
|
+
|
|
361
|
+
#### Locale of dates
|
|
362
|
+
|
|
363
|
+
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.
|
|
364
|
+
|
|
365
|
+
## Various hints when using the advanced sheets service
|
|
366
|
+
|
|
367
|
+
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.
|
|
368
|
+
|
|
369
|
+
### Advanced sheets updating cells
|
|
370
|
+
|
|
371
|
+
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.
|
|
372
|
+
|
|
373
|
+
In any case, I've implemented them all (note that this one that doesnt actually work in GAS - https://issuetracker.google.com/issues/423737982)
|
|
374
|
+
|
|
375
|
+
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.
|
|
376
|
+
|
|
377
|
+
If you want to see how these are all generated, see the constructor in services/advsheets/fakeadvsheets.
|
|
378
|
+
|
|
379
|
+
#### Handling multiple response variations and formats.
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
Something like this should do the trick.
|
|
384
|
+
````js
|
|
385
|
+
const extractPattern = (response) => {
|
|
386
|
+
// a plain pattern entered by UI, apps script or lax api call
|
|
387
|
+
if (is.string(response)) return response
|
|
388
|
+
// should be { type: "TYPE", pattern: "xxx"}
|
|
389
|
+
if (!is.object(response) || !Reflect.has(response, "pattern")) return null
|
|
390
|
+
return response.pattern
|
|
342
391
|
}
|
|
343
|
-
|
|
392
|
+
````
|
|
344
393
|
|
|
345
|
-
|
|
394
|
+
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.###"}
|
|
346
395
|
|
|
347
|
-
|
|
396
|
+
##### Numberformat default pattern
|
|
348
397
|
|
|
349
|
-
|
|
398
|
+
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" }
|
|
350
399
|
|
|
351
|
-
|
|
400
|
+
#### Text direction
|
|
352
401
|
|
|
353
|
-
|
|
402
|
+
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.
|
|
354
403
|
|
|
355
|
-
|
|
404
|
+
#### Horizontal alignment
|
|
356
405
|
|
|
357
|
-
|
|
406
|
+
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.
|
|
358
407
|
|
|
359
|
-
|
|
408
|
+
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.
|
|
360
409
|
|
|
361
|
-
|
|
410
|
+
#### Wrap and Wrap strategy
|
|
362
411
|
|
|
363
|
-
|
|
412
|
+
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.
|
|
364
413
|
|
|
365
|
-
|
|
414
|
+
The Apps Script issue for that is here https://issuetracker.google.com/issues/427134600
|
|
366
415
|
|
|
367
|
-
|
|
416
|
+
#### range.copyValuesToRange
|
|
368
417
|
|
|
369
|
-
|
|
418
|
+
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."
|
|
370
419
|
|
|
371
|
-
|
|
420
|
+
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
|
|
372
421
|
|
|
373
|
-
|
|
422
|
+
I'm pausing implementation on this one till I see what I should actually implement
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
#### TextRotation
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
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
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
````
|
|
434
|
+
Exception: The parameters ((class)) don't match the method signature for SpreadsheetApp.Range.setTextRotation.
|
|
435
|
+
````
|
|
436
|
+
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.
|
|
437
|
+
|
|
438
|
+
[See this issue for more information ](https://issuetracker.google.com/issues/425390984)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
Here's Gemini's verdict on textRotation
|
|
442
|
+
|
|
443
|
+
"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."
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
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.
|
|
447
|
+
|
|
448
|
+
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.
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
#### Dates and sheets advanced service
|
|
452
|
+
|
|
453
|
+
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.
|
|
454
|
+
````js
|
|
455
|
+
const dateToSerial = (date) => {
|
|
456
|
+
if (!is.date(date)) {
|
|
457
|
+
throw new Error(`dateToSerial is expecting a date but got ${is(date)}`)
|
|
458
|
+
}
|
|
459
|
+
// these are held in a serial number like in Excel, rather than JavaScript epoch
|
|
460
|
+
// so the epoch is actually Dec 30 1899 rather than Jan 1 1970
|
|
461
|
+
const epochCorrection = 2209161600000
|
|
462
|
+
const msPerDay = 24 * 60 * 60 * 1000
|
|
463
|
+
const adjustedMs = date.getTime() + epochCorrection
|
|
464
|
+
return adjustedMs / msPerDay
|
|
465
|
+
}
|
|
466
|
+
````
|
|
467
|
+
To enter this, you submit do this to create the value for your updateCells request body.
|
|
468
|
+
````js
|
|
469
|
+
const value = Sheets.newExtendedValue().setNumberValue(dateToSerial(value))
|
|
470
|
+
````
|
|
471
|
+
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.
|
|
472
|
+
|
|
473
|
+
#### UI settings
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
##### examples of UI settings not intuitively settable in GAS service
|
|
478
|
+
|
|
479
|
+
- allow multiple selections - needs the allowMultipleSelections set to true - you need to you advanced service to set this
|
|
480
|
+
- display style - chip - This needs the displayStyle property set to "CHIP" - you need to you advanced service to set this
|
|
481
|
+
- color for drop downs - haven't looked into this, but it's not possible via regular gas service.
|
|
482
|
+
|
|
483
|
+
###### showCustomUI
|
|
484
|
+
|
|
485
|
+
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.
|
|
486
|
+
|
|
487
|
+
In the Apps Script DataValidation builder, setting showCustomUi is achieved via the boolean 2nd argument(known as showDropdown) to requireValueInList() and requireValueInRange().
|
|
488
|
+
|
|
489
|
+
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".
|
|
490
|
+
|
|
491
|
+
## Enums
|
|
492
|
+
|
|
493
|
+
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.
|
|
374
494
|
|
|
375
|
-
|
|
495
|
+
## Auth
|
|
496
|
+
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,
|
|
376
497
|
|
|
377
498
|
## Help
|
|
378
499
|
|
|
@@ -381,4 +502,4 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
381
502
|
## Translations and writeups
|
|
382
503
|
|
|
383
504
|
- [mcpher](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
384
|
-
- [Russian version](README.RU.md) ([credit Alex Ivanov](https://github.com/oshliaer))
|
|
505
|
+
- [Russian version](README.RU.md) ([credit Alex Ivanov](https://github.com/oshliaer)) - needs updating
|
package/package.json
CHANGED
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
"node": ">=20.11.0"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@mcpher/
|
|
6
|
+
"@mcpher/fake-gasenum": "^1.0.2",
|
|
7
7
|
"@sindresorhus/is": "^7.0.1",
|
|
8
8
|
"archiver": "^7.0.1",
|
|
9
9
|
"get-stream": "^9.0.1",
|
|
10
|
-
"
|
|
11
|
-
"googleapis": "^144.0.0",
|
|
10
|
+
"googleapis": "^150.0.1",
|
|
12
11
|
"got": "^14.4.5",
|
|
13
12
|
"into-stream": "^8.0.1",
|
|
14
13
|
"keyv": "^5.2.3",
|
|
15
14
|
"keyv-file": "^5.1.1",
|
|
16
|
-
"make-synchronous": "^1.0.0",
|
|
17
15
|
"mime": "^4.0.6",
|
|
18
16
|
"sleep-synchronously": "^2.0.0",
|
|
17
|
+
"subsume": "^4.0.0",
|
|
19
18
|
"to-readable-stream": "^4.0.0",
|
|
20
19
|
"unzipper": "^0.12.3"
|
|
21
20
|
},
|
|
@@ -23,6 +22,9 @@
|
|
|
23
22
|
"scripts": {
|
|
24
23
|
"test": "cp mainlocal.js main.js && node --env-file=.env ./test/test.js",
|
|
25
24
|
"testdrive": "cp mainlocal.js main.js && node --env-file=.env ./test/testdrive.js execute",
|
|
25
|
+
"testsheetsdatavalidations": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsdatavalidations.js execute",
|
|
26
|
+
"testsheetspermissions": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetspermissions.js execute",
|
|
27
|
+
"testsheetsvalues": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsvalues.js execute",
|
|
26
28
|
"testsheets": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheets.js execute",
|
|
27
29
|
"testfetch": "cp mainlocal.js main.js && node --env-file=.env ./test/testfetch.js execute",
|
|
28
30
|
"testsession": "cp mainlocal.js main.js && node --env-file=.env ./test/testsession.js execute",
|
|
@@ -30,10 +32,13 @@
|
|
|
30
32
|
"teststores": "cp mainlocal.js main.js && node --env-file=.env ./test/teststores.js execute",
|
|
31
33
|
"testscriptapp": "cp mainlocal.js main.js && node --env-file=.env ./test/testscriptapp.js execute",
|
|
32
34
|
"testfiddler": "cp mainlocal.js main.js && node --env-file=.env ./test/testfiddler.js execute",
|
|
35
|
+
"testenums": "cp mainlocal.js main.js && node --env-file=.env ./test/testenums.js execute",
|
|
36
|
+
"testsheetssets": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetssets.js execute",
|
|
37
|
+
"testsheetsvui": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsvui.js execute",
|
|
33
38
|
"pub": "cp mainlocal.js main.js && npm publish --access public"
|
|
34
39
|
},
|
|
35
40
|
"name": "@mcpher/gas-fakes",
|
|
36
|
-
"version": "1.0.
|
|
41
|
+
"version": "1.0.10",
|
|
37
42
|
"main": "main.js",
|
|
38
43
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
|
39
44
|
"repository": "github:brucemcpherson/gas-fakes",
|
|
@@ -17,6 +17,7 @@ import { newFakeDrivePermissions } from './fakeadvdrivepermissions.js'
|
|
|
17
17
|
class FakeAdvDrive {
|
|
18
18
|
constructor() {
|
|
19
19
|
this.client = Proxies.guard(getAuthedClient())
|
|
20
|
+
this.__fakeObjectType ="Drive"
|
|
20
21
|
}
|
|
21
22
|
toString() {
|
|
22
23
|
return `AdvancedServiceIdentifier{name=drive, version=v3}`
|