@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.
Files changed (56) hide show
  1. package/README.md +287 -166
  2. package/package.json +10 -5
  3. package/src/services/advdrive/fakeadvdrive.js +1 -0
  4. package/src/services/advdrive/fakeadvdriveabout.js +1 -0
  5. package/src/services/advdrive/fakeadvdriveapps.js +1 -0
  6. package/src/services/advdrive/fakeadvdrivefiles.js +1 -0
  7. package/src/services/advdrive/fakeadvdrivepermissions.js +1 -0
  8. package/src/services/advsheets/fakeadvsheets.js +900 -225
  9. package/src/services/advsheets/fakeadvsheetsspreadsheets.js +25 -14
  10. package/src/services/advsheets/fakeadvvalues.js +2 -5
  11. package/src/services/commonclasses/fakeborder.js +92 -0
  12. package/src/services/commonclasses/fakeborders.js +44 -0
  13. package/src/services/{spreadsheetapp → commonclasses}/fakecolor.js +1 -4
  14. package/src/services/{spreadsheetapp → commonclasses}/fakecolorbase.js +3 -7
  15. package/src/services/{spreadsheetapp → commonclasses}/fakecolorbuilder.js +27 -4
  16. package/src/services/commonclasses/fakeprotection.js +109 -0
  17. package/src/services/{spreadsheetapp → commonclasses}/fakergbcolor.js +2 -6
  18. package/src/services/commonclasses/faketextdirection.js +19 -0
  19. package/src/services/commonclasses/faketextrotation.js +34 -0
  20. package/src/services/commonclasses/faketextstyle.js +66 -0
  21. package/src/services/commonclasses/faketextstylebuilder.js +278 -0
  22. package/src/services/{spreadsheetapp → commonclasses}/fakethemecolor.js +2 -5
  23. package/src/services/commonclasses/fakewrapstrategy.js +43 -0
  24. package/src/services/driveapp/fakedrivefile.js +4 -4
  25. package/src/services/driveapp/fakedrivemeta.js +5 -4
  26. package/src/services/enums/sheetsenums.js +270 -0
  27. package/src/services/session/fakesession.js +1 -1
  28. package/src/services/spreadsheetapp/datavalidationcriteriamapping.js +216 -0
  29. package/src/services/spreadsheetapp/fakedatavalidation.js +40 -0
  30. package/src/services/spreadsheetapp/fakedatavalidationbuilder.js +307 -0
  31. package/src/services/spreadsheetapp/fakesheet.js +73 -23
  32. package/src/services/spreadsheetapp/fakesheetrange.js +690 -326
  33. package/src/services/spreadsheetapp/fakesheetrangelist.js +1 -4
  34. package/src/services/spreadsheetapp/fakespreadsheet.js +86 -17
  35. package/src/services/spreadsheetapp/fakespreadsheetapp.js +74 -45
  36. package/src/services/spreadsheetapp/sheetrangehelpers.js +127 -0
  37. package/src/services/spreadsheetapp/sheetrangemakers.js +688 -0
  38. package/src/services/stores/fakestores.js +3 -0
  39. package/src/services/utilities/fakeutilities.js +22 -6
  40. package/src/support/auth.js +24 -7
  41. package/src/support/filesharers.js +1 -1
  42. package/src/support/helpers.js +26 -0
  43. package/src/support/patchedmakesynchronous.js +80 -0
  44. package/src/support/sheetutils.js +1 -0
  45. package/src/support/sxapi.js +1 -0
  46. package/src/support/sxauth.js +4 -7
  47. package/src/support/sxdrive.js +2 -0
  48. package/src/support/sxstorekit.js +20 -0
  49. package/src/support/syncit.js +26 -17
  50. package/src/support/utils.js +96 -23
  51. package/tsynk/t.mjs +10 -0
  52. package/tsynk/w.mjs +13 -0
  53. package/deprec/deprec-test.js +0 -1258
  54. package/src/services/typedefs.js +0 -301
  55. package/src/support/nummery.js +0 -30
  56. /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 just implemented a very limited 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).
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 .env_template in your own environments, then `npm i && npm test`.
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 indivually (for example `npm run testdrive`) or all with `npm test`
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
- Here's a simple example of how to get info on an access token made synchronous
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.7
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` - 25%
182
- - `SpreadsheetApp` - 25%
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
- Here's the code for `Utilities`
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
- ```js
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
- Here's how the proxies are registered
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
- ```js
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
- In short, the service us 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 has been initialized.
288
+ ##### Setting a relative date
262
289
 
263
- 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`.
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
- ### Iterators
292
+ Not all date validations have related RELATIVE versions. See later section for details.
266
293
 
267
- An iterator created by a generator does not have a `hasNext()` function, whereas GAS iterators do. To get round this, we can create a regular Node iterator, but introduce a wrapper so the constructor actually gets the first one, and `next()` uses the value we've already peeked at. Here's a wrapper to convert an iterator into a GAS style one.
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
- import { Proxies } from "./proxies.js";
271
- /**
272
- * this is a class to add a hasnext to a generator
273
- * @class Peeker
274
- *
275
- */
276
- class Peeker {
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
- * get the next value - actually its already got and storef in peeked
299
- * @returns {object} {value, done}
300
- */
301
- next() {
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
- And an example of usage, creating a parents iterator from a Drive API file.
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
- assert.object(file)
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
- function* filesink() {
327
- // the result tank, we just get them all by id
328
- let tank = file.parents.map(id => getFileById({ id, allow404: false }))
345
+ ```
346
+ SpreadsheetApp.newDataValidation().requireDate("=i1")
347
+ ```
329
348
 
330
- while (tank.length) {
331
- yield newFakeDriveFolder(tank.splice(0, 1)[0])
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
- // create the iterator
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
- // a regular iterator doesnt support the same methods
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
- ### Cache and Property services
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
- 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.
396
+ ##### Numberformat default pattern
348
397
 
349
- #### Script, user and document store varieties
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
- All 3 are supported for both properties and cache.
400
+ #### Text direction
352
401
 
353
- ##### scriptId
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
- 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.
404
+ #### Horizontal alignment
356
405
 
357
- ##### userId
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
- 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.
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
- ##### documentId
410
+ #### Wrap and Wrap strategy
362
411
 
363
- 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.
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
- ### Settings and temporary files
414
+ The Apps Script issue for that is here https://issuetracker.google.com/issues/427134600
366
415
 
367
- 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.
416
+ #### range.copyValuesToRange
368
417
 
369
- ## Noticed differences
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
- 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.
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
- ### Tradeoffs
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
- 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.
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/unit": "^1.1.11",
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
- "google-auth-library": "^9.15.0",
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.8",
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}`
@@ -4,6 +4,7 @@ import { notYetImplemented } from '../../support/helpers.js'
4
4
  class FakeAdvDriveAbout {
5
5
  constructor(drive) {
6
6
  this.toString = drive.toString
7
+ this.__fakeObjectType ="Drive.About"
7
8
  }
8
9
 
9
10
  // this is a schema and needs the fields parameter
@@ -12,6 +12,7 @@ class FakeAdvDriveApps {
12
12
  this.drive = drive
13
13
  this.name = 'Drive.Apps'
14
14
  this.apiProp = 'apps'
15
+ this.__fakeObjectType ="Drive.Apps"
15
16
  }
16
17
 
17
18
  toString() {
@@ -16,6 +16,7 @@ class FakeAdvDriveFiles {
16
16
  this.drive = drive
17
17
  this.name = 'Drive.Files'
18
18
  this.apiProp = apiProp
19
+ this.__fakeObjectType ="Drive.Files"
19
20
  }
20
21
 
21
22
  toString() {