@mcpher/gas-fakes 1.0.8 → 1.0.9
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 +153 -165
- package/package.json +6 -2
- 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 +2 -5
- package/src/services/advsheets/fakeadvsheetsspreadsheets.js +3 -1
- 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 +21 -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 +45 -0
- package/src/services/commonclasses/faketextstyle.js +53 -0
- package/src/services/commonclasses/faketextstylebuilder.js +273 -0
- package/src/services/{spreadsheetapp → commonclasses}/fakethemecolor.js +2 -5
- package/src/services/commonclasses/fakewrapstrategy.js +33 -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 +217 -0
- package/src/services/spreadsheetapp/fakedatavalidation.js +40 -0
- package/src/services/spreadsheetapp/fakedatavalidationbuilder.js +307 -0
- package/src/services/spreadsheetapp/fakesheet.js +68 -22
- package/src/services/spreadsheetapp/fakesheetrange.js +468 -234
- 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 +159 -0
- package/src/services/stores/fakestores.js +3 -0
- package/src/services/typedefs.js +2 -1
- package/src/services/utilities/fakeutilities.js +22 -6
- package/src/support/filesharers.js +1 -1
- package/src/support/helpers.js +26 -0
- package/src/support/sheetutils.js +1 -0
- package/src/support/sxstorekit.js +20 -0
- package/src/support/utils.js +77 -22
- package/deprec/deprec-test.js +0 -1258
- package/src/support/nummery.js +0 -30
- /package/src/services/{session → commonclasses}/fakeuser.js +0 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ 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
|
|
|
@@ -92,28 +92,7 @@ Although Apps Script supports async/await/promise syntax, it operates in blockin
|
|
|
92
92
|
|
|
93
93
|
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
94
|
|
|
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
|
-
```
|
|
95
|
+
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
96
|
|
|
118
97
|
### OAuth
|
|
119
98
|
|
|
@@ -172,14 +151,14 @@ This was a little problematic to sequence, but I wanted to make sure that any GA
|
|
|
172
151
|
|
|
173
152
|
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
153
|
|
|
175
|
-
v1.0.
|
|
154
|
+
v1.0.8
|
|
176
155
|
|
|
177
156
|
- `DriveApp` - 50%
|
|
178
157
|
- `ScriptApp` - almost all
|
|
179
158
|
- `UrlFetchApp` - 80%
|
|
180
159
|
- `Utilities` - almost all
|
|
181
160
|
- `Sheets` - 25%
|
|
182
|
-
- `SpreadsheetApp` -
|
|
161
|
+
- `SpreadsheetApp` - 60%
|
|
183
162
|
- `CacheService` - 80%
|
|
184
163
|
- `PropertiesService` - 80%
|
|
185
164
|
- `Session` - almost all
|
|
@@ -195,184 +174,193 @@ Tests for all methods are added as we go to the cumulative unit tests and run on
|
|
|
195
174
|
|
|
196
175
|
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
176
|
|
|
198
|
-
|
|
177
|
+
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.
|
|
199
178
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
179
|
+
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`.
|
|
180
|
+
|
|
181
|
+
### Iterators
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
### Cache and Property services
|
|
186
|
+
|
|
187
|
+
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.
|
|
188
|
+
|
|
189
|
+
#### Script, user and document store varieties
|
|
190
|
+
|
|
191
|
+
All 3 are supported for both properties and cache.
|
|
192
|
+
|
|
193
|
+
##### scriptId
|
|
194
|
+
|
|
195
|
+
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.
|
|
196
|
+
|
|
197
|
+
##### userId
|
|
198
|
+
|
|
199
|
+
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.
|
|
200
|
+
|
|
201
|
+
##### documentId
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
|
|
205
|
+
### Settings and temporary files
|
|
206
|
+
|
|
207
|
+
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.
|
|
208
|
+
|
|
209
|
+
## Noticed differences
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
213
|
+
### Tradeoffs
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
## Oddities
|
|
218
|
+
|
|
219
|
+
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.
|
|
220
|
+
|
|
221
|
+
### Formats and styles
|
|
222
|
+
|
|
223
|
+
When getting formats with the sheets API, there are 2 types
|
|
224
|
+
|
|
225
|
+
- userEnteredFormat - any formats a user (or an apps script function) has explicitly set
|
|
226
|
+
- effectiveFormat - what rendered format actually looks like
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
|
|
230
|
+
### Values
|
|
231
|
+
|
|
232
|
+
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. I haven't figured out how to handles dates yet.
|
|
233
|
+
|
|
234
|
+
Here is how I've implemented getting and setting values.
|
|
235
|
+
|
|
236
|
+
- getValues() uses { valueRenderOption: 'UNFORMATTED_VALUE' }
|
|
237
|
+
- setValues() uses { valueInputOption: "RAW" } (as opposed to 'USER_ENTERED')
|
|
238
|
+
- getDisplayValues() { valueRenderOption: 'FORMATTED_VALUE' }
|
|
239
|
+
|
|
240
|
+
### Data Validation
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
#### Criteria types
|
|
245
|
+
|
|
246
|
+
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.
|
|
247
|
+
|
|
248
|
+
Here's an example Gemini reponse during multiple back and forward conversations.
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
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
252
|
```
|
|
227
253
|
|
|
228
|
-
|
|
254
|
+
The file 'fakedatavalidationcriteria.js' has a list of the final mappings between the 2.
|
|
255
|
+
|
|
256
|
+
#### Relative dates
|
|
257
|
+
|
|
258
|
+
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.
|
|
259
|
+
|
|
260
|
+
As usual, Gemini is no help in this.
|
|
229
261
|
|
|
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
|
-
};
|
|
262
|
+
```
|
|
263
|
+
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
264
|
```
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
##### Setting a relative date
|
|
262
267
|
|
|
263
|
-
There
|
|
268
|
+
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
269
|
|
|
265
|
-
|
|
270
|
+
Not all date validations have related RELATIVE versions. See later section for details.
|
|
266
271
|
|
|
267
|
-
|
|
272
|
+
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
273
|
|
|
269
274
|
```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
|
-
}
|
|
288
|
-
|
|
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
|
-
}
|
|
296
|
-
|
|
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
|
-
}
|
|
312
|
-
|
|
313
|
-
export const newPeeker = (...args) => Proxies.guard(new Peeker(...args));
|
|
275
|
+
const rule = SpreadsheetApp.newDataValidation()
|
|
276
|
+
.withCriteria(SpreadsheetApp.DataValidationCriteria.DATE_EQUAL_TO_RELATIVE, [
|
|
277
|
+
SpreadsheetApp.RelativeDate.TODAY,
|
|
278
|
+
])
|
|
279
|
+
.build();
|
|
280
|
+
const range = sheet.getRange("b30");
|
|
281
|
+
range.setDataValidation(rule);
|
|
314
282
|
```
|
|
315
283
|
|
|
316
|
-
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
##### Getting a relative date
|
|
287
|
+
|
|
288
|
+
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.
|
|
317
289
|
|
|
290
|
+
This is what the sheets API returns.
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
{"condition":{"type":"DATE_EQ","values":[{"relativeDate":"TODAY"}]}}
|
|
318
294
|
```
|
|
319
|
-
const getParentsIterator = ({
|
|
320
|
-
file
|
|
321
|
-
}) => {
|
|
322
295
|
|
|
323
|
-
|
|
324
|
-
assert.array(file.parents)
|
|
296
|
+
Which would be translated into a criteria type of DATE_EQUAL_TO_RELATIVE in GAS, with the value SpreadsheetApp.DataValidation.Criteria.TODAY
|
|
325
297
|
|
|
326
|
-
|
|
327
|
-
// the result tank, we just get them all by id
|
|
328
|
-
let tank = file.parents.map(id => getFileById({ id, allow404: false }))
|
|
298
|
+
#### datavalidation enum and relative dates
|
|
329
299
|
|
|
330
|
-
|
|
331
|
-
yield newFakeDriveFolder(tank.splice(0, 1)[0])
|
|
332
|
-
}
|
|
333
|
-
}
|
|
300
|
+
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).
|
|
334
301
|
|
|
335
|
-
|
|
336
|
-
const parentsIt = filesink()
|
|
302
|
+
These 3 relatives exist as keys of SpreadsheetApp.DataValidationCriteria, but none of the other DATE enum values exist
|
|
337
303
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
304
|
+
- DATE_AFTER_RELATIVE
|
|
305
|
+
- DATE_BEFORE_RELATIVE
|
|
306
|
+
- DATE_EQUAL_TO_RELATIVE
|
|
341
307
|
|
|
342
|
-
|
|
308
|
+
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.
|
|
309
|
+
|
|
310
|
+
#### datavalidation with formulas
|
|
311
|
+
|
|
312
|
+
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.
|
|
313
|
+
|
|
314
|
+
In other words - here's what happens in GAS when you retrieve a data validation that has had a formula used as its value
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
console.log (cb.getCriteriaType().toString()) // DATE_EQUAL_TO
|
|
318
|
+
console.log (cb.getCriteriaValues()) // [ '=I1' ]
|
|
343
319
|
```
|
|
344
320
|
|
|
345
|
-
|
|
321
|
+
and yet, you get the error 'The parameters (String) don't match the method signature for SpreadsheetApp.DataValidationBuilder.requireDate.' with this.
|
|
346
322
|
|
|
347
|
-
|
|
323
|
+
```
|
|
324
|
+
SpreadsheetApp.newDataValidation().requireDate("=i1")
|
|
325
|
+
```
|
|
348
326
|
|
|
349
|
-
|
|
327
|
+
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
328
|
|
|
351
|
-
|
|
329
|
+
```
|
|
330
|
+
SpreadsheetApp.newDataValidation().withCriteria(SpreadsheetApp.DataValidationCriteria.DATE_AFTER,['=i1']).build()
|
|
331
|
+
```
|
|
352
332
|
|
|
353
|
-
|
|
333
|
+
I'm leaving these same behaviors in place, and you would need to use the same workarounds as you do in GAS.
|
|
354
334
|
|
|
355
|
-
|
|
335
|
+
#### mixing real dates and relative dates
|
|
356
336
|
|
|
357
|
-
|
|
337
|
+
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.
|
|
358
338
|
|
|
359
|
-
|
|
339
|
+
#### Locale of dates
|
|
360
340
|
|
|
361
|
-
|
|
341
|
+
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.
|
|
362
342
|
|
|
363
|
-
|
|
343
|
+
#### UI settings
|
|
364
344
|
|
|
365
|
-
|
|
345
|
+
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.
|
|
366
346
|
|
|
367
|
-
|
|
347
|
+
##### examples of UI settings not intuitively settable in GAS service
|
|
368
348
|
|
|
369
|
-
|
|
349
|
+
- allow multiple selections - needs the allowMultipleSelections set to true - you need to you advanced service to set this
|
|
350
|
+
- display style - chip - This needs the displayStyle property set to "CHIP" - you need to you advanced service to set this
|
|
351
|
+
- color for drop downs - haven't looked into this, but it's not possible via regular gas service.
|
|
370
352
|
|
|
371
|
-
|
|
353
|
+
###### showCustomUI
|
|
372
354
|
|
|
373
|
-
|
|
355
|
+
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.
|
|
356
|
+
|
|
357
|
+
In the Apps Script DataValidation builder, setting showCustomUi is achieved via the boolean 2nd argument(known as showDropdown) to requireValueInList() and requireValueInRange().
|
|
358
|
+
|
|
359
|
+
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".
|
|
360
|
+
|
|
361
|
+
## Enums
|
|
374
362
|
|
|
375
|
-
|
|
363
|
+
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.
|
|
376
364
|
|
|
377
365
|
## Help
|
|
378
366
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "cp mainlocal.js main.js && node --env-file=.env ./test/test.js",
|
|
25
25
|
"testdrive": "cp mainlocal.js main.js && node --env-file=.env ./test/testdrive.js execute",
|
|
26
|
+
"testsheetsdatavalidations": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsdatavalidations.js execute",
|
|
27
|
+
"testsheetspermissions": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetspermissions.js execute",
|
|
28
|
+
"testsheetsvalues": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsvalues.js execute",
|
|
26
29
|
"testsheets": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheets.js execute",
|
|
27
30
|
"testfetch": "cp mainlocal.js main.js && node --env-file=.env ./test/testfetch.js execute",
|
|
28
31
|
"testsession": "cp mainlocal.js main.js && node --env-file=.env ./test/testsession.js execute",
|
|
@@ -30,10 +33,11 @@
|
|
|
30
33
|
"teststores": "cp mainlocal.js main.js && node --env-file=.env ./test/teststores.js execute",
|
|
31
34
|
"testscriptapp": "cp mainlocal.js main.js && node --env-file=.env ./test/testscriptapp.js execute",
|
|
32
35
|
"testfiddler": "cp mainlocal.js main.js && node --env-file=.env ./test/testfiddler.js execute",
|
|
36
|
+
"testenums": "cp mainlocal.js main.js && node --env-file=.env ./test/testenums.js execute",
|
|
33
37
|
"pub": "cp mainlocal.js main.js && npm publish --access public"
|
|
34
38
|
},
|
|
35
39
|
"name": "@mcpher/gas-fakes",
|
|
36
|
-
"version": "1.0.
|
|
40
|
+
"version": "1.0.9",
|
|
37
41
|
"main": "main.js",
|
|
38
42
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
|
39
43
|
"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}`
|
|
@@ -5,17 +5,14 @@ import { Proxies } from '../../support/proxies.js'
|
|
|
5
5
|
import { notYetImplemented } from '../../support/helpers.js'
|
|
6
6
|
import { getAuthedClient } from '../spreadsheetapp/shapis.js'
|
|
7
7
|
import {newFakeAdvSheetsSpreadsheets} from './fakeadvsheetsspreadsheets.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* the advanced Sheets Apps Script service faked
|
|
11
|
-
* @class FakeAdvSheets
|
|
12
|
-
*/
|
|
8
|
+
|
|
13
9
|
|
|
14
10
|
|
|
15
11
|
|
|
16
12
|
class FakeAdvSheets {
|
|
17
13
|
constructor() {
|
|
18
14
|
this.client = Proxies.guard(getAuthedClient())
|
|
15
|
+
this.__fakeObjectType ="Sheets"
|
|
19
16
|
const props = [
|
|
20
17
|
'newGridData',
|
|
21
18
|
'newConditionalFormatRule',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Advanced sheets service
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
|
|
6
6
|
import { Proxies } from '../../support/proxies.js'
|
|
7
7
|
import { Syncit } from '../../support/syncit.js'
|
|
@@ -18,6 +18,8 @@ import { getWorkbookEntry, setWorkbookEntry, clearWorkbookCache } from "../../su
|
|
|
18
18
|
class FakeAdvSheetsSpreadsheets {
|
|
19
19
|
constructor(sheets) {
|
|
20
20
|
|
|
21
|
+
this.__fakeObjectType ="Sheets.Spreadsheets"
|
|
22
|
+
|
|
21
23
|
const props = [
|
|
22
24
|
'getByDataFilter',
|
|
23
25
|
'DeveloperMetadata',
|
|
@@ -2,10 +2,7 @@ import { Proxies } from '../../support/proxies.js'
|
|
|
2
2
|
import { notYetImplemented, ssError } from '../../support/helpers.js'
|
|
3
3
|
import { Syncit } from '../../support/syncit.js'
|
|
4
4
|
import { getWorkbookEntry, setWorkbookEntry, clearWorkbookCache } from "../../support/sheetscache.js"
|
|
5
|
-
|
|
6
|
-
* @file
|
|
7
|
-
* @imports ../typedefs.js
|
|
8
|
-
*/
|
|
5
|
+
|
|
9
6
|
// private properties are identified with leading __
|
|
10
7
|
// this will signal to the proxy handler that it's okay to set them
|
|
11
8
|
|
|
@@ -30,7 +27,7 @@ class FakeSheetValues {
|
|
|
30
27
|
* @returns {FakeSheetValues}
|
|
31
28
|
*/
|
|
32
29
|
constructor(sheets) {
|
|
33
|
-
|
|
30
|
+
this.__fakeObjectType ="Sheets.Spreadsheets.Values"
|
|
34
31
|
this.__sheets = sheets
|
|
35
32
|
const props = [
|
|
36
33
|
'batchClearByDataFilter',
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js'
|
|
2
|
+
import { newFakeColorBuilder } from '../commonclasses/fakecolorbuilder.js'
|
|
3
|
+
import { Utils } from '../../support/utils.js'
|
|
4
|
+
import { BorderStyle } from '../enums/sheetsenums.js'
|
|
5
|
+
const {is, robToHex} = Utils
|
|
6
|
+
const BLACK = '#000000'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* create a new FakeBorder instance
|
|
11
|
+
* @param {...any} args
|
|
12
|
+
* @returns {FakeBorder}
|
|
13
|
+
*/
|
|
14
|
+
export const newFakeBorder = (...args) => {
|
|
15
|
+
return Proxies.guard(new FakeBorder(...args))
|
|
16
|
+
}
|
|
17
|
+
// https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/cells#Border
|
|
18
|
+
class FakeBorder {
|
|
19
|
+
/**
|
|
20
|
+
* @param {FakeColor} color
|
|
21
|
+
* @param {object} p result from sheets border query
|
|
22
|
+
* @param {BorderStyle} p.style border style
|
|
23
|
+
* @param {number} width border width
|
|
24
|
+
* @param {Color} color ...to be discovered
|
|
25
|
+
* @param {ColorStyle} colorStyle {rgbColor|themeColor}
|
|
26
|
+
* @returns {FakeBorder} a border
|
|
27
|
+
*/
|
|
28
|
+
// it's possible that the border info will be null
|
|
29
|
+
// we need to produce various defaults to support the object type
|
|
30
|
+
// rules deduced from GAS tests seem to be
|
|
31
|
+
// color - an UNSUPPORTED colorType - this is the default
|
|
32
|
+
// borderStyle - null
|
|
33
|
+
constructor(apiResult) {
|
|
34
|
+
const {color, style, width, colorStyle } = apiResult || {}
|
|
35
|
+
|
|
36
|
+
// the border style dotted/dashed etc
|
|
37
|
+
this.__borderStyle = null
|
|
38
|
+
if (style) {
|
|
39
|
+
if (!BorderStyle[style]){
|
|
40
|
+
throw new Error(`unknown border style ${style} received from api`)
|
|
41
|
+
}
|
|
42
|
+
this.__borderStyle = BorderStyle[style]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// TODO not sure what to do with this information yet
|
|
46
|
+
// since width is part of the definition of borderstyle
|
|
47
|
+
this.__width = width
|
|
48
|
+
|
|
49
|
+
// if both colorstyle and color are provided, colorstyle takes precendence
|
|
50
|
+
// i think there is some legacy stuff being handled with this multiple option returns
|
|
51
|
+
// so it's likely that the colorStyle will always be returned
|
|
52
|
+
const colorBuilder = newFakeColorBuilder()
|
|
53
|
+
if (colorStyle && is.nonEmptyObject(colorStyle)) {
|
|
54
|
+
|
|
55
|
+
if (colorStyle.themeColor) {
|
|
56
|
+
// TODO whats the default theme ?
|
|
57
|
+
colorBuilder.setThemeColor(colorStyle.themeColor)
|
|
58
|
+
} else if (colorStyle.rgbColor) {
|
|
59
|
+
const rgb = is.emptyObject(colorStyle.rgbColor) ? BLACK : robToHex(colorStyle.rgbColor)
|
|
60
|
+
colorBuilder.setRgbColor(rgb)
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error("border colorstyle missing both rgbColor and themeColor")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
} else if (color && is.nonEmptyMap(color)) {
|
|
66
|
+
// in this case its just an rgbcolor - and I think only here for legacy - i doubt if this will ever be called
|
|
67
|
+
colorBuilder.setColor(robToHex(color))
|
|
68
|
+
|
|
69
|
+
} else if (!apiResult) {
|
|
70
|
+
// this can happen if we got a null from the API so we allow the builder to create an UNSUPPORTED type
|
|
71
|
+
// no action required here
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error ('no color types were provided for border')
|
|
74
|
+
}
|
|
75
|
+
this.__color = colorBuilder.build()
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* @returns {BorderStyle} borderStyle border style
|
|
79
|
+
*/
|
|
80
|
+
getBorderStyle() {
|
|
81
|
+
return this.__borderStyle
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* @returns {FakeColor}
|
|
85
|
+
*/
|
|
86
|
+
getColor() {
|
|
87
|
+
return this.__color
|
|
88
|
+
}
|
|
89
|
+
toString() {
|
|
90
|
+
return 'Border'
|
|
91
|
+
}
|
|
92
|
+
}
|