@mcpher/gas-fakes 1.0.1 → 1.0.2

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.RU.md ADDED
@@ -0,0 +1,315 @@
1
+ # Подтверждение концепции реализации среды Apps Script на Node
2
+
3
+ Я использую clasp/vscode для разработки приложений Google Apps Script (GAS), но при использовании нативных сервисов GAS возникает слишком много переходов туда-сюда к IDE GAS во время тестирования. Я поставил себе цель реализовать фейковую версию среды выполнения GAS на Node, чтобы хотябы сделать тесты локально.
4
+
5
+ Это всего лишь демонстрационная реализация, поэтому я реализовал очень ограниченное количество сервисов и методов, но все сложные части уже на месте, так что остается только много рутинной работы (к которой я с энтузиазмом приглашаю любых заинтересованных соавторов).
6
+
7
+ ## Начало работы
8
+
9
+ You can get the package from npm
10
+
11
+ ```sh
12
+ npm i @mcpher/gas-fakes
13
+ ```
14
+
15
+ Идея заключается в том, что вы можете запускать локально на Node сервисы GAS (пока только те, что реализованны), и он (Node) будет использовать различные API Google Workspace для эмуляции того, что произошло бы, если бы вы запустили то же самое в среде GAS.
16
+
17
+ ### Облачный проект
18
+
19
+ У вас нет доступа к облачному проекту, поддерживаемому GAS, поэтому вам нужно создать проект GCP для использования локально. Чтобы продублировать управление OAuth, обрабатываемое GAS, мы будем использовать Application Default Credentials. В этом репозитории есть некоторые скрипты для настройки и тестирования этих данных. Как только вы настроите облачный проект, перейдите в папку `shells` и добавьте свой `project id` в `setaccount.sh` и
20
+
21
+ ### Тестируйте
22
+
23
+ Рекомендую использовать тестовый проект, включенный в репозиторий, чтобы убедиться, что все настроено правильно. Он использует Fake DriveApp service для проверки Auth и т.д. Просто измените преднастройки на значения, присутствующие в вашем собственном Диске, затем `npm i && npm test`. Обратите внимание, что я использую [юнит-тестировщик](https://ramblings.mcpher.com/apps-script-test-runner-library-ported-to-node/), который работает как в GAS, так и в Node, поэтому те же самые тесты будут выполняться в обоих средах.
24
+
25
+ ### Передача в GAS
26
+
27
+ Скрипт `togas.sh` переместит ваши файлы в gas - просто установите папки `SOURCE` и `TARGET` в скрипте. Убедитесь, что у вас есть манифест `appsscript.json` в папке `SOURCE`, поскольку **gas-fakes** читает его для обработки OAuth на Node.
28
+
29
+ Вы можете написать проект, который будет работать на Node и вызывать сервисы GAS, и он также будет работать в среде GAS без изменений кода, за исключением того, что на стороне Node у вас есть этот один импорт
30
+
31
+ ```js
32
+ // all the fake services are here
33
+ import '@mcpher/gas-fakes/main.js'
34
+ ```
35
+
36
+ `togas.sh` удалит `imports` и `exports` по пути к Apps Script, который их не поддерживает.
37
+
38
+ ## Подход
39
+
40
+ Google не опубликовали детали о среде исполнения GAS (насколько мне известно). Мы знаем то, что они раньше работали на эмуляторе JavaScript под названием [Rhino](https://ramblings.mcpher.com/gassnippets2/what-javascript-engine-is-apps-script-running-on/), основанном на Java, но несколько лет назад перешели на среду исполнения V8. Помимо этого, мы не знаем почти ничего, кроме того, что они работают где-то на серверах Google.
41
+
42
+ Было 3 основные сложные проблемы, которые нужно было преодолеть, чтобы это заработало
43
+
44
+ - GAS полностью синхронный, тогда как замена вызовов API Workspace на Node асинхронная.
45
+ - GAS автоматически обрабатывает инициализацию OAuth из файла манифеста, тогда как нам нужна дополнительный код или альтернативные подходы на Node.
46
+ - Сервисные синглтоны (например, DriveApp) автоматически инициализируются и доступны в глобальном пространстве имен, тогда как в Node им нужна некоторая пост-AUTH инициализация, упорядочивание инициализации и экспозиция.
47
+ - Итераторы GAS не такие же, как стандартные итераторы, так как у них есть метод `hasNext()` и они не ведут себя одинаково.
48
+
49
+ Помимо этого, реализация - это просто много рутинной работы. Вот как я справился с этими 3 проблемами.
50
+
51
+ ### Синхронность против Асинхронности
52
+
53
+ Хотя Apps Script поддерживает синтаксис async/await/promise, он работает в блокирующем режиме. Я действительно не хотел настаивать на асинхронном кодировании в коде, ориентированном на GAS, поэтому мне нужно было найти способ эмулировать то, что, вероятно, делает среда GAS.
54
+
55
+ Так как асинхронность является фундаментальной для Node, нет простого способа преобразовать асинхронность в синхронность. Однако существует такое понятие как *[child-process](https://nodejs.org/api/child_process.html#child-process)*, который вы можете запустить для выполнения вещей, и он имеет метод [`execSync`](https://nodejs.org/api/child_process.html#child_processexecsynccommand-options), который задерживает возврат из дочернего процесса до тех пор, пока очередь переданного обещания не будет полностью завершена. Таким образом, самым простым решением является запуск асинхронного метода в дочернем процессе, ожидание его завершения и возврат результатов синхронно. Я обнаружил, что [Sindre Sorhus](https://github.com/sindresorhus) использует этот подход с [make-synchronous](https://github.com/sindresorhus/make-synchronous), поэтому и я использую это.
56
+
57
+ Вот простой пример того, как получить информацию о токене доступа синхронно
58
+
59
+ ```js
60
+ /**
61
+ * a sync version of token checking
62
+ * @param {string} token the token to check
63
+ * @returns {object} access token info
64
+ */
65
+ const fxCheckToken = (accessToken) => {
66
+
67
+ // now turn all that into a synchronous function - it runs as a subprocess, so we need to start from scratch
68
+ const fx = makeSynchronous(async accessToken => {
69
+ const { default: got } = await import('got')
70
+ const tokenInfo = await got(`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${accessToken}`).json()
71
+ return tokenInfo
72
+ })
73
+
74
+ const result = fx(accessToken)
75
+ return result
76
+ }
77
+ ```
78
+
79
+ ### OAuth
80
+
81
+ Здесь мы имеем два компонента решения.
82
+
83
+ #### Приложение с параметрами доступа по умолчанию / Application default credentials (ADC)
84
+
85
+ Чтобы избежать большого количества специфичного для Node кода и учетных данных, но все же обрабатывать OAuth, я решил, что мы можем просто полагаться на ADC. Это проблема, о которой я уже писал здесь [Application Default Credentials with Google Cloud and Workspace APIs](https://ramblings.mcpher.com/application-default-credentials-with-google-cloud-and-workspace-apis/)
86
+
87
+ Для настройки этого установите ID вашего проекта GCP и дополнительные области, которые вам нужны, в `shells/setaccount.sh`. В этом примере я сохраняю обычные области ADC и добавляю дополнительную область для доступа к Drive
88
+
89
+ ```sh
90
+ # project ID
91
+ P=YOUR_GCP_PROJECT_ID
92
+
93
+ # config to activate - multiple configs can each be named
94
+ # here we're working on the default project configuration
95
+ AC=default
96
+
97
+ # these are the ones it sets by default - take some of these out if you want to minimize access
98
+ DEFAULT_SCOPES="https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/drive,openid,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/sqlservice.login"
99
+
100
+ # these are the ones we want to add (note comma at beginning)
101
+ EXTRA_SCOPES=",https://www.googleapis.com/auth/drive"
102
+
103
+ .....etc
104
+ ```
105
+
106
+ После настройки самого проекта вы можете выполнить скрипт, и это настроит ваш ADC, чтобы запускать любые службы, которым требуются области, которые вы добавили.
107
+
108
+ ##### примечание
109
+
110
+ Хотя может быть искушение добавить `https://www.googleapis.com/auth/script.external_request`, это не обязательно для ADC и, на самом деле, вызовет ошибку. Конечно, вам понадобится это в вашем манифесте Apps script.
111
+
112
+ ##### тестирование ADC
113
+
114
+ `shells/testtoken.sh` можно использовать для проверки, что вы можете сгенерировать токен с достаточной областью. В этом примере я проверяю, что могу получить доступ к файлу, которым владею. Измените `id` на один из ваших.
115
+
116
+ ```sh
117
+ # check tokens have scopes required for DRIVE access
118
+ # set below to a fileid on drive you have access to
119
+ FILE_ID=SOME_FILE_ID
120
+
121
+ ....etc
122
+ ```
123
+
124
+ Рекомендую сделать это, чтобы убедиться, что Auth работает нормально, прежде чем начинать кодировать свое приложение.
125
+
126
+ #### Файл манифеста
127
+
128
+ **gas-fakes** читает файл манифеста, чтобы узнать, какие области вам нужны в проекте, использует библиотеку Google Auth для попытки авторизации и имеет `ScriptApp.getOauthToken()`, чтобы возвращать достаточно специфицированный токен, как это делает среда GAS. Просто убедитесь, что у вас есть `appsscript.json` в той же папке, что и ваш основной скрипт.
129
+
130
+ ### Глобальная инициализация
131
+
132
+ Это было немного проблематично для реализации последовательности инициализации, но я хотел убедиться, что любые GAS сервисы, которые имитируются, доступны и инициализированы на стороне Node, как и в GAS. На момент написания (подмножество методов) этих сервисов реализовано.
133
+
134
+ v1.0.0 подтверждение концепции для
135
+
136
+ - `DriveApp`
137
+ - `ScriptApp`
138
+ - `UrlFetchApp`
139
+ - `Utilities`
140
+
141
+ #### Прокси и globalThis
142
+
143
+ Каждый сервис имеет `FakeClass`, но мне нужно было, чтобы цикл Auth был инициирован и выполнен перед тем, как сделать их публичными. Использование прокси был самым простым подходом.
144
+
145
+ Вот код для `ScriptApp`
146
+
147
+ ```js
148
+ /**
149
+ * adds to global space to mimic Apps Script behavior
150
+ */
151
+ const name = "ScriptApp"
152
+
153
+ if (typeof globalThis[name] === typeof undefined) {
154
+
155
+ console.log ('setting script app to global')
156
+
157
+ const getApp = () => {
158
+
159
+ // if it hasn't been intialized yet then do that
160
+ if (!_app) {
161
+
162
+ // we also need to do the manifest scopes thing and the project id
163
+ const projectId = Syncit.fxGetProjectId()
164
+ const manifest = Syncit.fxGetManifest()
165
+ Auth.setProjectId (projectId)
166
+ Auth.setManifestScopes(manifest)
167
+
168
+ _app = {
169
+ getOAuthToken,
170
+ requireAllScopes,
171
+ requireScopes,
172
+ AuthMode: {
173
+ FULL: 'FULL'
174
+ }
175
+ }
176
+
177
+
178
+ }
179
+ // this is the actual driveApp we'll return from the proxy
180
+ return _app
181
+ }
182
+
183
+
184
+ Proxies.registerProxy(name, getApp)
185
+
186
+ }
187
+ ```
188
+
189
+ Вот как регистрируются прокси
190
+
191
+ ```js
192
+
193
+ /**
194
+ * diverts the property get to another object returned by the getApp function
195
+ * @param {function} a function to get the proxy object to substitutes
196
+ * @returns {function} a handler for a proxy
197
+ */
198
+ const getAppHandler = (getApp) => {
199
+ return {
200
+
201
+ get(_, prop, receiver) {
202
+ // this will let the caller know we're not really running in Apps Script
203
+ return (prop === 'isFake') ? true : Reflect.get(getApp(), prop, receiver);
204
+ },
205
+
206
+ ownKeys(_) {
207
+ return Reflect.ownKeys(getApp())
208
+ }
209
+ }
210
+ }
211
+
212
+ const registerProxy = (name, getApp) => {
213
+ const value = new Proxy({}, getAppHandler(getApp))
214
+ // add it to the global space to mimic what apps script does
215
+ Object.defineProperty(globalThis, name, {
216
+ value,
217
+ enumerable: true,
218
+ configurable: false,
219
+ writable: false,
220
+ });
221
+ }
222
+ ```
223
+
224
+ Коротко говоря, сервис регистрируется как пустой объект, но при любой попытке получить доступ к нему фактически возвращает другой объект, который обрабатывает запрос. В примере `ScriptApp` является пустым объектом, но доступ к `ScriptApp.getOAuthToken()` возвращает объект ложный (Fake) `ScriptApp`, который был инициализирован.
225
+
226
+ Также есть тест, чтобы проверить, запущены ли вы в GAS или на Node - `ScriptApp.isFake`
227
+
228
+ ### Итераторы
229
+
230
+ Итератор, созданный генератором, не имеет функции `hasNext()`, в то время как итераторы GAS имеют. Чтобы обойти это, мы можем создать обычный итератор Node, но ввести обертку, чтобы конструктор фактически получил первый элемент, а `next()` использовал значение, которое мы уже посмотрели. Вот обертка для преобразования итератора в стиль GAS
231
+
232
+ ```js
233
+ import { Proxies } from './proxies.js'
234
+ /**
235
+ * this is a class to add a hasnext to a generator
236
+ * @class Peeker
237
+ *
238
+ */
239
+ class Peeker {
240
+ /**
241
+ * @constructor
242
+ * @param {function} generator the generator function to add a hasNext() to
243
+ * @returns {Peeker}
244
+ */
245
+ constructor(generator) {
246
+ this.generator = generator
247
+ // in order to be able to do a hasnext we have to actually get the value
248
+ // this is the next value stored
249
+ this.peeked = generator.next()
250
+ }
251
+
252
+ /**
253
+ * we see if there's a next if the peeked at is all over
254
+ * @returns {Boolean}
255
+ */
256
+ hasNext () {
257
+ return !this.peeked.done
258
+ }
259
+
260
+ /**
261
+ * get the next value - actually its already got and storef in peeked
262
+ * @returns {object} {value, done}
263
+ */
264
+ next () {
265
+ if (!this.hasNext()) {
266
+ // TODO find out what driveapp does
267
+ throw new Error ('iterator is exhausted - there is no more')
268
+ }
269
+ // instead of returning the next, we return the prepeeked next
270
+ const value = this.peeked.value
271
+ this.peeked = this.generator.next()
272
+ return value
273
+ }
274
+ }
275
+
276
+ export const newPeeker = (...args) => Proxies.guard(new Peeker (...args))
277
+ ```
278
+
279
+ И пример использования, создающий итератор родителей из файла API Drive
280
+
281
+ ```js
282
+ /**
283
+ * this gets an intertor to fetch all the parents meta data
284
+ * @param {FakeDriveMeta} {file} the meta data
285
+ * @returns {object} {Peeker}
286
+ */
287
+ const getParentsIterator = ({
288
+ file
289
+ }) => {
290
+
291
+ Utils.assert.object(file)
292
+ Utils.assert.array(file.parents)
293
+
294
+ function* filesink() {
295
+ // the result tank, we just get them all by id
296
+ let tank = file.parents.map(id => getFileById({ id, allow404: false }))
297
+
298
+ while (tank.length) {
299
+ yield newFakeDriveFolder(tank.splice(0, 1)[0])
300
+ }
301
+ }
302
+
303
+ // create the iterator
304
+ const parentsIt = filesink()
305
+
306
+ // a regular iterator doesnt support the same methods
307
+ // as Apps Script so we'll fake that too
308
+ return newPeeker(parentsIt)
309
+
310
+ }
311
+ ```
312
+
313
+ ## Помощь
314
+
315
+ Как я уже упоминал ранее, чтобы развивать это дальше, мне понадобится много помощи для расширения поддерживаемых методов и сервисов - поэтому, если вы считаете, что это будет полезно для вас, и хотите сотрудничать, пожалуйста, свяжитесь со мной по [bruce@mcpher.com](mailto:bruce@mcpher.com) и мы поговорим.
package/README.md CHANGED
@@ -1,63 +1,90 @@
1
1
  # A proof of concept implementation of Apps Script Environment on Node
2
2
 
3
- I use clasp/vscode to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and fowards to the GAS IDE going while testing. I set myself the ambition of implementing fake version of the GAS runtime environment on Node so I could at least do some testing locally.
3
+ I use clasp/vscode to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and fowards to the GAS IDE going while testing. I set myself the ambition of implementing fake version of the GAS runtime environment on Node so I could at least do some testing locally.
4
4
 
5
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).
6
6
 
7
- ## getting started
7
+ ## progress
8
+
9
+ This is a pretty huge task, so I'm working on adding services a little bit at a time, with usually just a few methods added in each release.
10
+
11
+ ## Getting started
8
12
 
9
13
  You can get the package from npm
10
14
 
11
- ````
15
+ ```sh
12
16
  npm i @mcpher/gas-fakes
13
- ````
17
+ ```
14
18
 
15
19
  The idea is that you can run GAS services (so far implemented) locally on Node, and it will use various Google Workspace APIS to emulate what would happen if you were to run the same thing in the GAS environment.
16
20
 
17
-
18
21
  ### Cloud project
19
22
 
20
- You don't have access to the GAS maintained cloud project, so you'll need to create a GCP project to use locally. In order to duplicate the OAuth management handled by GAS, we'll use Application Default Credentials. There are some scripts in this repo to set up and test these. Once you've set up a clud project go to the shells folder and add your project id to setaccount.sh and
23
+ You don't have access to the GAS maintained cloud project, so you'll need to create a GCP project to use locally. In order to duplicate the OAuth management handled by GAS, we'll use Application Default Caredentials. There re some scripts in this repo to set up and test these. Once you've set up a cloud project go to the shells folder and add your `project id` to `setaccount.sh` and
21
24
 
22
25
  ### Testing
23
26
 
24
- 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 to values present in your own Drive, then npm i && npm test. 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.
27
+ 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 to values present in your own Drive, then `npm i && npm test`. 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.
25
28
 
29
+ ### Settings
30
+
31
+ gasfakes.json holds various location and behavior parameters to inform about your Node environment. It's not required on GAS as you can't change anything over there. If you don't have one, it'll create one for you and use some sensible defaults. Here's an example of one with the defaults. It should be in the same folder as your main script.
32
+ ````
33
+ {
34
+ "manifest": "./appsscript.json",
35
+ "clasp": "./.clasp.json",
36
+ "documentId": null,
37
+ "cache": "/tmp/gas-fakes/cache",
38
+ "properties": "/tmp/gas-fakes/properties",
39
+ "scriptId": "1ey2fr74m4n9fwaqi9dsx9ye"
40
+ }
41
+ ````
42
+ | property | type | default | description |
43
+ | ----------- | --------------- | ------------------------------------ | --------------------------------------------------------------------------------------------- |
44
+ | manifest | string | ./appsscript.json | the manifest path and name relative to your main module |
45
+ | clasp | string | ./clasp.json | where to look for an optional clasp file |
46
+ | documentId | string | null | a bound document id. This will allow testing of container bound script. The documentId will become your activeDocument (for the appropriate service) |
47
+ | cache | string | /tmp/gas-fakes/cache | gas-fakes uses a local file to emulate apps script's CacheService. This is where it should put the files |
48
+ | properties | string | /tmp/gas-fakes/properties | gas-fakes uses a local file to emulate apps script's PropertiesService. This is where it should put the files. You may want to put it somewhere other than /tmp to avoid accidental deletion, but don't put it in a place that'll get commited to public git repo |
49
+ | scriptId | string | from clasp, or some random value | If you have a clasp file, it'll pick up the scriptId from there. If not you can enter your scriptId manually, or just leave it to create a fake one. It's use for the moment is to return something useful from ScriptApp.getScriptId() and to partition the cache and properties stores |
50
+
51
+ More on all this later.
26
52
 
27
53
  ### Pushing to GAS
28
54
 
29
- The script togas.sh will move your files to gas - just set the SOURCE and TARGET folders in the script. Make sure you have an appsscript.json manifest in the SOURCE folder, as gas-fakes reads that to handle OAuth on Node.
55
+ The script togas.sh will move your files to GAS - just set the `SOURCE` and `TARGET` folders in the script. Make sure you have an `appsscript.json` manifest in the `SOURCE` folder, as **gas-fakes** reads that to handle OAuth on Node.
30
56
 
31
- You can write your project to run on Node and call GAS services, and it will also run on the GAS environment with no code changes, except on the node side you have this one import
57
+ You can write your project to run on Node and call GAS services, and it will also run on the GAS environment with no code changes, except on the Node side you have this one import
32
58
 
33
- ````
59
+ ```sh
34
60
  // all the fake services are here
35
61
  import '@mcpher/gas-fakes/main.js'
36
- ````
62
+ ```
37
63
 
38
64
  togas.sh will remove imports and exports on the way to apps script, which doesnt support them.
39
65
 
40
-
41
66
  ## Approach
42
67
 
43
- Google have not made details about the GAS run time public (as far as I know). What we do know is that it used to run on a Java based JavaScript emulator [Rhino](https://ramblings.mcpher.com/gassnippets2/what-javascript-engine-is-apps-script-running-on/) but a few years ago moved to a V8 runtime. Beyond that, we don't know anything much other than it runs on Google Servers somewhere.
68
+ Google have not made details about the GAS run time public (as far as I know). What we do know is that it used to run on a Java based JavaScript emulator [Rhino](https://ramblings.mcpher.com/gassnippets2/what-javascript-engine-is-apps-script-running-on/) but a few years ago moved to a V8 runtime. Beyond that, we don't know anything much other than it runs on Google Servers somewhere.
44
69
 
45
70
  There were 3 main sticky problems to overcome to get this working
71
+
46
72
  - GAS is entirely synchronous, whereas the replacement calls to Workspace APIS on Node are all asynchrounous.
47
73
  - GAS handles OAuth initialization from the manifest file automatically, whereas we need some additional coding or alternative approaches on Node.
48
74
  - The service singletons (eg. DriveApp) are all intialized and available in the global space automatically, whereas in Node they need some post AUTH intialization, sequencing intialization and exposure.
49
75
  - GAS iterators aren't the same as standard iterators, as they have a hasNext() method and don't behave in the same way.
50
76
 
51
- Beyond that, implementation is just a lot of busy work. Here's how I've dealt with these 3 problems.
77
+ Beyond that, implementation is just a lot of busy work. If you are interested, here's how I've dealt with these 3 problems.
52
78
 
53
79
  ### Sync versus Async
54
80
 
55
- Although Apps Script supports async/await/promise syntax, it operates in blocking mode. I didn't really want to have to insist on async coding in code targeted at GAS, so I needed to find a way to emulate what the GAS environment probably does.
81
+ Although Apps Script supports async/await/promise syntax, it operates in blocking mode. I didn't really want to have to insist on async coding in code targeted at GAS, so I needed to find a way to emulate what the GAS environment probably does.
56
82
 
57
83
  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.
58
84
 
59
85
  Here's a simple example of how to get info on an access token made synchronous
60
- ````
86
+
87
+ ```js
61
88
  /**
62
89
  * a sync version of token checking
63
90
  * @param {string} token the token to check
@@ -75,7 +102,8 @@ const fxCheckToken = (accessToken) => {
75
102
  const result = fx(accessToken)
76
103
  return result
77
104
  }
78
- ````
105
+ ```
106
+
79
107
  ### OAuth
80
108
 
81
109
  There's 2 pieces to this solution.
@@ -84,9 +112,9 @@ There's 2 pieces to this solution.
84
112
 
85
113
  In order to avoid a bunch of Node specific code and credentials, yet still handle OAuth, I figured that we could simply rely on ADC. This is a problem I already wrote about here [Application Default Credentials with Google Cloud and Workspace APIs](https://ramblings.mcpher.com/application-default-credentials-with-google-cloud-and-workspace-apis/)
86
114
 
87
- To set this up, set your GCP project ID and the extra scopes you'll need in shells/setaccount.sh. In this example I'm retaining the usual ADC scopes, and adding an extra scope to be able to access Drive.
115
+ To set this up, set your GCP project ID and the extra scopes you'll need in `shells/setaccount.sh`. In this example I'm retaining the usual ADC scopes, and adding an extra scope to be able to access Drive.
88
116
 
89
- ````
117
+ ```sh
90
118
  # project ID
91
119
  P=YOUR_GCP_PROJECT_ID
92
120
 
@@ -101,49 +129,53 @@ DEFAULT_SCOPES="https://www.googleapis.com/auth/userinfo.email,https://www.googl
101
129
  EXTRA_SCOPES=",https://www.googleapis.com/auth/drive"
102
130
 
103
131
  .....etc
104
- ````
105
- Now you can execute this and it will set up your ADC to be able to run any services that require the scopes you add.
132
+ ```
133
+
134
+ Now you can execute this and it will set up your ADC to be able to run any services that require the scopes you add.
106
135
 
107
136
  ##### note
108
- Although you may be tempted to add "https://www.googleapis.com/auth/script.external_request", it's not necessary for the ADC and in fact will generate an error. You will of course need it in your Apps script manifest.
137
+
138
+ Although you may be tempted to add `https://www.googleapis.com/auth/script.external_request`, it's not necessary for the ADC and in fact will generate an error. You will of course need it in your Apps script manifest.
109
139
 
110
140
  ##### testing ADC
111
141
 
112
- shells/testtoken.sh can test that you can generate a token with sufficient scope. In this example, I'm checking that I can access a file I own. Change the id to one of your own.
113
- ````
142
+ `shells/testtoken.sh` can test that you can generate a token with sufficient scope. In this example, I'm checking that I can access a file I own. Change the id to one of your own.
143
+
144
+ ```js
114
145
  # check tokens have scopes required for DRIVE access
115
146
  # set below to a fileid on drive you have access to
116
147
  FILE_ID=SOME_FILE_ID
117
148
 
118
149
  ....etc
119
- ````
150
+ ```
120
151
 
121
152
  I recommend you do this to make sure Auth it's all good before you start coding up your app.
122
153
 
123
-
124
154
  #### Manifest file
125
155
 
126
- gas-fakes reads the manifest file to see which scopes you need in your project, uses the Google Auth library to attempt to authorizes them and has ScriptApp.getOauthToken() return a sufficiently specced token, just as the GAS environment does. Just make sure you have an appsscript.json in the same folder as your main script.
156
+ **gas-fakes** reads the manifest file to see which scopes you need in your project, uses the Google Auth library to attempt to authorizes them and has `ScriptApp.getOauthToken()` return a sufficiently specced token, just as the GAS environment does. Just make sure you have an `appsscript.json` in the same folder as your main script.
127
157
 
128
- ### global intialization
158
+ ### Global intialization
129
159
 
130
- This was a little problematic to sequence, but I wanted to make sure that any GAS services being imitated were available and initialized on the Node side, just as they are in GAS. At the time of writing (a subset of the methods of) these services are implemented.
160
+ This was a little problematic to sequence, but I wanted to make sure that any GAS services being imitated were available and initialized on the Node side, just as they are in GAS. At the time of writing these services are implemented. Only a subset of methods are currently available - the rest are work in progress.
131
161
 
132
- v1.0.0 proof of concept for
162
+ v1.0.1
163
+ - `DriveApp`
164
+ - `ScriptApp`
165
+ - `UrlFetchApp`
166
+ - `Utilities`
167
+ - `Sheets`
168
+ - `CacheService`
169
+ - `PropertiesService`
133
170
 
134
- - DriveApp
135
- - ScriptApp
136
- - UrlFetchApp
137
- - Utilities
138
-
139
-
140
- #### Proxies and globalThis
171
+ #### Proxies and globalThis
141
172
 
142
173
  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.
143
174
 
144
- Here's the code for ScriptApp
175
+ Here's the code for `ScriptApp`
176
+
177
+ ```js
145
178
 
146
- ````
147
179
  /**
148
180
  * adds to global space to mimic Apps Script behavior
149
181
  */
@@ -151,18 +183,14 @@ const name = "ScriptApp"
151
183
 
152
184
  if (typeof globalThis[name] === typeof undefined) {
153
185
 
154
- console.log ('setting script app to global')
186
+ // initializing auth etc
187
+ Syncit.fxInit()
155
188
 
189
+ console.log(`setting ${name} to global`)
156
190
  const getApp = () => {
157
191
 
158
192
  // if it hasn't been intialized yet then do that
159
193
  if (!_app) {
160
-
161
- // we also need to do the manifest scopes thing and the project id
162
- const projectId = Syncit.fxGetProjectId()
163
- const manifest = Syncit.fxGetManifest()
164
- Auth.setProjectId (projectId)
165
- Auth.setManifestScopes(manifest)
166
194
 
167
195
  _app = {
168
196
  getOAuthToken,
@@ -183,10 +211,11 @@ if (typeof globalThis[name] === typeof undefined) {
183
211
  Proxies.registerProxy(name, getApp)
184
212
 
185
213
  }
186
- ````
214
+ ```
187
215
 
188
216
  Here's how the proxies are registered
189
- ````
217
+
218
+ ```js
190
219
 
191
220
  /**
192
221
  * diverts the property get to another object returned by the getApp function
@@ -217,16 +246,17 @@ const registerProxy = (name, getApp) => {
217
246
  writable: false,
218
247
  });
219
248
  }
220
- ````
249
+ ```
221
250
 
222
- 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.
251
+ 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.
223
252
 
224
- There's also a test available to see if you are running in GAS or on Node - ScriptApp.isFake
253
+ 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`.
225
254
 
226
255
  ### Iterators
227
256
 
228
- 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.
229
- ````
257
+ 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.
258
+
259
+ ```js
230
260
  import { Proxies } from './proxies.js'
231
261
  /**
232
262
  * this is a class to add a hasnext to a generator
@@ -271,21 +301,17 @@ class Peeker {
271
301
  }
272
302
 
273
303
  export const newPeeker = (...args) => Proxies.guard(new Peeker (...args))
274
- ````
304
+ ```
275
305
 
276
306
  And an example of usage, creating a parents iterator from a Drive API file.
307
+
277
308
  ````
278
- /**
279
- * this gets an intertor to fetch all the parents meta data
280
- * @param {FakeDriveMeta} {file} the meta data
281
- * @returns {object} {Peeker}
282
- */
283
309
  const getParentsIterator = ({
284
310
  file
285
311
  }) => {
286
312
 
287
- Utils.assertType(file, "object")
288
- Utils.assertType(file.parents, "array")
313
+ assert.object(file)
314
+ assert.array(file.parents)
289
315
 
290
316
  function* filesink() {
291
317
  // the result tank, we just get them all by id
@@ -305,9 +331,39 @@ const getParentsIterator = ({
305
331
 
306
332
  }
307
333
  ````
308
- ## Help
309
334
 
310
- As I mentioned earlier, to take this further, I'm going to need a lot of help to extend the methods and services supported - so if you feel this would be useful to you, and would like to collaborate, please ping me on @bruce@mcpher.com and we'll talk.
311
335
 
336
+ ### Cache and Property services
337
+
338
+ 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.
339
+
340
+
341
+ #### Script, user and document store varieties
342
+
343
+ All 3 are supported for both properties and cache.
344
+
345
+ ##### scriptId
346
+
347
+ 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.
348
+
349
+ ##### userId
350
+
351
+ 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.
352
+
353
+ ##### documentId
354
+
355
+ 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.
356
+
357
+ ### Settings and temporary files
358
+
359
+ 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.
360
+
361
+
362
+ ## Help
363
+
364
+ As I mentioned earlier, to take this further, I'm going to need a lot of help to extend the methods and services supported - so if you feel this would be useful to you, and would like to collaborate, please ping me on [bruce@mcpher.com](mailto:bruce@mcpher.com) and we'll talk.
312
365
 
366
+ ## Translations and writeups
313
367
 
368
+ - [mcpher](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
369
+ - [Russian version](README.RU.md) ([credit Alex Ivanov](https://github.com/oshliaer))