@mcpher/gas-fakes 1.0.2 → 1.0.4

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 CHANGED
@@ -36,7 +36,7 @@ gasfakes.json holds various location and behavior parameters to inform about you
36
36
  "documentId": null,
37
37
  "cache": "/tmp/gas-fakes/cache",
38
38
  "properties": "/tmp/gas-fakes/properties",
39
- "scriptId": "1ey2fr74m4n9fwaqi9dsx9ye"
39
+ "scriptId": "1bc79bd3-fe02-425f-9653-525e5ae0b678"
40
40
  }
41
41
  ````
42
42
  | property | type | default | description |
@@ -157,9 +157,9 @@ I recommend you do this to make sure Auth it's all good before you start coding
157
157
 
158
158
  ### Global intialization
159
159
 
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.
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 and classes are implemented. However, only a subset of methods are currently available for some of them - the rest are work in progress.
161
161
 
162
- v1.0.1
162
+ v1.0.3
163
163
  - `DriveApp`
164
164
  - `ScriptApp`
165
165
  - `UrlFetchApp`
@@ -167,49 +167,43 @@ v1.0.1
167
167
  - `Sheets`
168
168
  - `CacheService`
169
169
  - `PropertiesService`
170
+ - `Session`
171
+ - `Blob`
172
+ - `User`
170
173
 
171
174
  #### Proxies and globalThis
172
175
 
173
176
  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.
174
177
 
175
- Here's the code for `ScriptApp`
178
+ Here's the code for `Utilities`
176
179
 
177
180
  ```js
178
181
 
179
182
  /**
180
183
  * adds to global space to mimic Apps Script behavior
181
184
  */
182
- const name = "ScriptApp"
185
+ import { Proxies } from '../../support/proxies.js'
186
+ import { newFakeUtilities } from './fakeutilities.js';
183
187
 
184
- if (typeof globalThis[name] === typeof undefined) {
185
188
 
186
- // initializing auth etc
187
- Syncit.fxInit()
189
+ // This will eventually hold a proxy for Utilties
190
+ let _app = null
188
191
 
189
- console.log(`setting ${name} to global`)
192
+ /**
193
+ * adds to global space to mimic Apps Script behavior
194
+ */
195
+ const name = "Utilities"
196
+ if (typeof globalThis[name] === typeof undefined) {
190
197
  const getApp = () => {
191
-
192
- // if it hasn't been intialized yet then do that
198
+ // if it hasnt been intialized yet then do that
193
199
  if (!_app) {
194
-
195
- _app = {
196
- getOAuthToken,
197
- requireAllScopes,
198
- requireScopes,
199
- AuthMode: {
200
- FULL: 'FULL'
201
- }
202
- }
203
-
204
-
200
+ console.log (`setting ${name} to global`)
201
+ _app = newFakeUtilities()
205
202
  }
206
203
  // this is the actual driveApp we'll return from the proxy
207
204
  return _app
208
205
  }
209
-
210
-
211
- Proxies.registerProxy(name, getApp)
212
-
206
+ Proxies.registerProxy (name, getApp)
213
207
  }
214
208
  ```
215
209
 
@@ -349,14 +343,18 @@ The local version may have no knowledge of the Apps ScriptId. If you are using c
349
343
  ##### userId
350
344
 
351
345
  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
346
  ##### documentId
354
347
 
355
348
  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
349
 
357
350
  ### Settings and temporary files
358
351
 
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.
352
+ 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.
353
+
354
+
355
+ ## Noticed differences
356
+
357
+ I'll make a note here on trivial implementation differences. The main will be slight differences in error message text, which I'll normalize over time. Please report any differences in behavior you find in the repo issues.
360
358
 
361
359
 
362
360
  ## Help
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "@sindresorhus/is": "^7.0.1",
4
+ "archiver": "^7.0.1",
4
5
  "get-stream": "^9.0.1",
5
6
  "google-auth-library": "^9.15.0",
6
7
  "googleapis": "^144.0.0",
@@ -9,7 +10,8 @@
9
10
  "keyv-file": "^5.1.1",
10
11
  "make-synchronous": "^1.0.0",
11
12
  "mime": "^4.0.6",
12
- "sleep-synchronously": "^2.0.0"
13
+ "sleep-synchronously": "^2.0.0",
14
+ "unzipper": "^0.12.3"
13
15
  },
14
16
  "type": "module",
15
17
  "scripts": {
@@ -17,7 +19,7 @@
17
19
  "pub": "npm publish --access public"
18
20
  },
19
21
  "name": "@mcpher/gas-fakes",
20
- "version": "1.0.2",
22
+ "version": "1.0.4",
21
23
  "main": "main.js",
22
24
  "description": "A proof of concept implementation of Apps Script Environment on Node",
23
25
  "repository": "github:brucemcpherson/gas-fakes",
package/src/index.js CHANGED
@@ -4,3 +4,5 @@ import './services/urlfetchapp/app.js'
4
4
  import './services/utilities/app.js'
5
5
  import './services/sheets/app.js'
6
6
  import './services/stores/app.js'
7
+ import './services/session/app.js'
8
+ import './services/advdrive/app.js'
@@ -0,0 +1,31 @@
1
+ // fake Apps Script DriveApp
2
+ /**
3
+ * the idea here is to create a global entry for the singleton
4
+ * before we actually have everything we need to create it.
5
+ * We do this by using a proxy, intercepting calls to the
6
+ * initial sigleton and diverting them to a completed one
7
+ */
8
+ import { newFakeAdvDrive} from './fakeadvdrive.js'
9
+ import { Proxies } from '../../support/proxies.js'
10
+
11
+ // This will eventually hold a proxy for DriveApp
12
+ let _app = null
13
+
14
+ /**
15
+ * adds to global space to mimic Apps Script behavior
16
+ */
17
+ const name = "Drive"
18
+ if (typeof globalThis[name] === typeof undefined) {
19
+
20
+ const getApp = () => {
21
+ // if it hasne been intialized yet then do that
22
+ if (!_app) {
23
+ _app = newFakeAdvDrive()
24
+ }
25
+ // this is the actual driveApp we'll return from the proxy
26
+ return _app
27
+ }
28
+
29
+ Proxies.registerProxy (name, getApp)
30
+
31
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Advanced drive service
3
+ */
4
+ import { Proxies } from '../../support/proxies.js'
5
+ import { notYetImplemented } from '../../support/constants.js'
6
+
7
+ class FakeAdvDrive {
8
+ constructor() {
9
+
10
+ }
11
+ toString() {
12
+ return `AdvancedServiceIdentifier{name=drive, version=v3}`
13
+ }
14
+ getVersion() {
15
+ return 'v3'
16
+ }
17
+ get Files() {
18
+ return newFakeAdvDriveFiles(this)
19
+ }
20
+ get About() {
21
+ return newFakeAdvDriveAbout(this)
22
+ }
23
+ get Accessproposals() {
24
+ return notYetImplemented
25
+ }
26
+ get Apps() {
27
+ return notYetImplemented
28
+ }
29
+ get Changes() {
30
+ return notYetImplemented
31
+ }
32
+ get Channels() {
33
+ return notYetImplemented
34
+ }
35
+ get Comments() {
36
+ return notYetImplemented
37
+ }
38
+ get Drives() {
39
+ return notYetImplemented
40
+ }
41
+ get Operations() {
42
+ return notYetImplemented
43
+ }
44
+ get Permissions() {
45
+ return notYetImplemented
46
+ }
47
+ get Replies() {
48
+ return notYetImplemented
49
+ }
50
+ get Revisions() {
51
+ return notYetImplemented
52
+ }
53
+ get Teamdrives() {
54
+ return notYetImplemented
55
+ }
56
+
57
+ }
58
+
59
+ class FakeAdvDriveAbout {
60
+ constructor(drive) {
61
+ this.toString = drive.toString
62
+ }
63
+
64
+ // this is a schema and needs the fields parameter
65
+ get() {
66
+ return notYetImplemented
67
+ }
68
+ }
69
+
70
+ class FakeAdvDriveFiles {
71
+ constructor(drive) {
72
+ this.toString = drive.toString
73
+ }
74
+
75
+ listLabels() {
76
+ return notYetImplemented
77
+ }
78
+
79
+ emptyTrash() {
80
+ return notYetImplemented
81
+ }
82
+
83
+ update() {
84
+ return notYetImplemented
85
+ }
86
+
87
+ list() {
88
+ return notYetImplemented
89
+ }
90
+ remove() {
91
+ return notYetImplemented
92
+ }
93
+
94
+ download() {
95
+ return notYetImplemented
96
+ }
97
+
98
+ modifyLabels() {
99
+ return notYetImplemented
100
+ }
101
+
102
+ watch() {
103
+ return notYetImplemented
104
+ }
105
+
106
+ get() {
107
+ return notYetImplemented
108
+ }
109
+
110
+ create() {
111
+ return notYetImplemented
112
+ }
113
+
114
+ generateIds() {
115
+ return notYetImplemented
116
+ }
117
+
118
+ copy() {
119
+ return notYetImplemented
120
+ }
121
+
122
+ export() {
123
+ return notYetImplemented
124
+ }
125
+
126
+ }
127
+
128
+ const newFakeAdvDriveFiles = (...args) => Proxies.guard(new FakeAdvDriveFiles(...args))
129
+ const newFakeAdvDriveAbout = (...args) => Proxies.guard(new FakeAdvDriveAbout(...args))
130
+ export const newFakeAdvDrive = (...args) => Proxies.guard(new FakeAdvDrive)
131
+
132
+
133
+
134
+ /* methods to implement
135
+ toString: [Function],
136
+ getVersion: [Function],
137
+ newTeamDriveRestrictions: [Function],
138
+ newTeamDrive: [Function],
139
+ newLabelFieldModification: [Function],
140
+ newFileImageMediaMetadataLocation: [Function],
141
+ newRevision: [Function],
142
+ newComment: [Function],
143
+ newFile: [Function],
144
+ newContentRestriction: [Function],
145
+ newDrive: [Function],
146
+ newDriveCapabilities: [Function],
147
+ newFileVideoMediaMetadata: [Function],
148
+ newDriveBackgroundImageFile: [Function],
149
+ newResolveAccessProposalRequest: [Function],
150
+ newFileLabelInfo: [Function],
151
+ newTeamDriveBackgroundImageFile: [Function],
152
+ newFileContentHints: [Function],
153
+ newPermission: [Function],
154
+ newFileLinkShareMetadata: [Function],
155
+ newFileImageMediaMetadata: [Function],
156
+ newFileCapabilities: [Function],
157
+ newCommentQuotedFileContent: [Function],
158
+ newReply: [Function],
159
+ newFileContentHintsThumbnail: [Function],
160
+ newModifyLabelsRequest: [Function],
161
+ newUser: [Function],
162
+ newLabel: [Function],
163
+ newDownloadRestriction: [Function],
164
+ newLabelModification: [Function],
165
+ newPermissionPermissionDetails: [Function],
166
+ newDriveRestrictions: [Function],
167
+ newPermissionTeamDrivePermissionDetails: [Function],
168
+ newFileShortcutDetails: [Function],
169
+ newChannel: [Function],
170
+ newTeamDriveCapabilities: [Function],
171
+ About: { toString: [Function], get: [Function] },
172
+
173
+
174
+
175
+
176
+ Accessproposals:
177
+ { toString: [Function],
178
+ resolve: [Function],
179
+ get: [Function],
180
+ list: [Function] },
181
+ Apps: { toString: [Function], get: [Function], list: [Function] },
182
+ Changes:
183
+ { toString: [Function],
184
+ getStartPageToken: [Function],
185
+ watch: [Function],
186
+ list: [Function] },
187
+ Channels: { toString: [Function], stop: [Function] },
188
+ Comments:
189
+ { toString: [Function],
190
+ get: [Function],
191
+ create: [Function],
192
+ update: [Function],
193
+ list: [Function],
194
+ remove: [Function] },
195
+ Drives:
196
+ { toString: [Function],
197
+ hide: [Function],
198
+ get: [Function],
199
+ create: [Function],
200
+ update: [Function],
201
+ list: [Function],
202
+ remove: [Function],
203
+ unhide: [Function] },
204
+ Files:
205
+ { toString: [Function],
206
+ listLabels: [Function],
207
+ emptyTrash: [Function],
208
+ update: [Function],
209
+ list: [Function],
210
+ remove: [Function],
211
+ download: [Function],
212
+ modifyLabels: [Function],
213
+ watch: [Function],
214
+ get: [Function],
215
+ create: [Function],
216
+ generateIds: [Function],
217
+ copy: [Function],
218
+ export: [Function] },
219
+ Operations:
220
+ { toString: [Function],
221
+ cancel: [Function],
222
+ get: [Function],
223
+ list: [Function],
224
+ remove: [Function] },
225
+ Permissions:
226
+ { toString: [Function],
227
+ get: [Function],
228
+ create: [Function],
229
+ update: [Function],
230
+ list: [Function],
231
+ remove: [Function] },
232
+ Replies:
233
+ { toString: [Function],
234
+ get: [Function],
235
+ create: [Function],
236
+ update: [Function],
237
+ list: [Function],
238
+ remove: [Function] },
239
+ Revisions:
240
+ { toString: [Function],
241
+ get: [Function],
242
+ update: [Function],
243
+ list: [Function],
244
+ remove: [Function] },
245
+ Teamdrives:
246
+ { toString: [Function],
247
+ get: [Function],
248
+ create: [Function],
249
+ update: [Function],
250
+ list: [Function],
251
+ remove: [Function] } }
252
+ */
@@ -92,9 +92,9 @@ if (typeof globalThis[name] === typeof undefined) {
92
92
  // initializing auth etc
93
93
  Syncit.fxInit()
94
94
 
95
- console.log(`setting ${name} to global`)
96
95
  const getApp = () => {
97
96
 
97
+ console.log(`setting ${name} to global`)
98
98
  // if it hasn't been intialized yet then do that
99
99
  if (!_app) {
100
100
 
@@ -0,0 +1,33 @@
1
+
2
+ /**
3
+ * fake Apps Script Session
4
+ * the idea here is to create a global entry for the singleton
5
+ * before we actually have everything we need to create it.
6
+ * We do this by using a proxy, intercepting calls to the
7
+ * initial sigleton and diverting them to a completed one
8
+ */
9
+
10
+ import { Proxies } from '../../support/proxies.js'
11
+ import { newFakeSession } from './fakesession.js'
12
+
13
+ let _app = null
14
+
15
+ /**
16
+ * adds to global space to mimic Apps Script behavior
17
+ */
18
+ const name = "Session"
19
+ if (typeof globalThis[name] === typeof undefined) {
20
+
21
+ const getApp = () => {
22
+ // if it hasn't been intialized yet then do that
23
+ if (!_app) {
24
+ console.log(`setting ${name} to global`)
25
+ _app = newFakeSession()
26
+ }
27
+ // this is the actual driveApp we'll return from the proxy
28
+ return _app
29
+ }
30
+
31
+ Proxies.registerProxy(name, getApp)
32
+
33
+ }
@@ -0,0 +1,45 @@
1
+ import { newFakeUser} from './fakeuser.js'
2
+ import { Auth } from '../../support/auth.js'
3
+ import { Proxies } from '../../support/proxies.js'
4
+
5
+ class FakeSession {
6
+ constructor () {
7
+ this._activeUser = newFakeUser (Auth.getTokenInfo())
8
+ }
9
+ getActiveUser() {
10
+ return this._activeUser
11
+ }
12
+ /**
13
+ * there's no difference between active/effective on node
14
+ * @returns {string}
15
+ */
16
+ getEffectiveUser() {
17
+ return this.getActiveUser()
18
+ }
19
+ /**
20
+ * @returns {string}
21
+ */
22
+ getActiveUserLocale() {
23
+ const lang = process.env.LANG || ''
24
+ // it'll be a format like en_US.UTF-8 so we need to drop the encoding to be like apps script
25
+ return lang.replace(/\..*/,'')
26
+ }
27
+ /**
28
+ * this'll come from the manifest on Node (on Apps Script it'll be where the user is running from)
29
+ * it's the same as the timezone
30
+ * @returns {string}
31
+ */
32
+ getScriptTimeZone() {
33
+ return Auth.getTimeZone()
34
+ }
35
+ /**
36
+ * this'll be an encrypted user ID - same as the one used for property/cache stores identification
37
+ * it's the same as the timezone
38
+ * @returns {string}
39
+ */
40
+ getTemporaryActiveUserKey() {
41
+ return Auth.getHashedUserId()
42
+ }
43
+ }
44
+
45
+ export const newFakeSession = (...args) => Proxies.guard (new FakeSession(...args))
@@ -0,0 +1,32 @@
1
+ import { Proxies } from '../../support/proxies.js'
2
+
3
+ /**
4
+ * returned by Session.getActiveUser,getEffectiveUser()
5
+ * the only method documented nowadays is getEmail()
6
+ * @class FakeUser
7
+ */
8
+ class FakeUser {
9
+ /**
10
+ * @param {object} p tokeninfo
11
+ * @param {string} p.email email
12
+ * @returns {FakeUser}
13
+ */
14
+ constructor ({email}) {
15
+ this.__email = email
16
+ }
17
+ getEmail () {
18
+ return this.__email
19
+ }
20
+ toString () {
21
+ return this.getEmail()
22
+ }
23
+ }
24
+
25
+ /**
26
+ * create a new FakeUser instance
27
+ * @param {...any} args
28
+ * @returns {FakeUser}
29
+ */
30
+ export const newFakeUser = (...args) => {
31
+ return Proxies.guard(new FakeUser(...args))
32
+ }
@@ -17,13 +17,14 @@ let _app = null
17
17
  const name = "SpreadsheetApp"
18
18
 
19
19
  if (typeof globalThis[name] === typeof undefined) {
20
- console.log (`setting ${name} to global`)
20
+
21
21
  /**
22
22
  * @returns {FakeSpreadsheetApp}
23
23
  */
24
24
  const getApp = () => {
25
25
  // if it hasnt been intialized yet then do that
26
26
  if (!_app) {
27
+ console.log (`setting ${name} to global`)
27
28
  _app = newFakeSpreadsheetApp()
28
29
  }
29
30
  // this is the actual driveApp we'll return from the proxy
@@ -21,11 +21,12 @@ let _cacheApp = null
21
21
  */
22
22
  const registerApp = (_app, name, kind) => {
23
23
  if (typeof globalThis[name] === typeof undefined) {
24
- console.log(`setting ${name} to global`)
24
+
25
25
 
26
26
  const getApp = () => {
27
27
  // if it hasnt been intialized yet then do that
28
28
  if (!_app) {
29
+ console.log(`setting ${name} to global`)
29
30
  _app = newFakeService(kind)
30
31
  }
31
32
  // this is the actual driveApp we'll return from the proxy
@@ -160,9 +160,9 @@ class FakeStore {
160
160
  const scriptId = Auth.getScriptId()
161
161
  const documentId = Auth.getDocumentId()
162
162
 
163
- let fileName = `${k}-${t}-${scriptId}`
163
+ let fileName = `${k}${t}-${scriptId}`
164
164
  if (this.type === StoreType.USER) {
165
- fileName += `-${Auth.getUserId()}`
165
+ fileName += `-${Auth.getHashedUserId()}`
166
166
  }
167
167
  else if (this.type === StoreType.DOCUMENT && documentId) {
168
168
  fileName += `-${documentId}`
@@ -69,10 +69,11 @@ let _app = null
69
69
  */
70
70
  const name = "UrlFetchApp"
71
71
  if (typeof globalThis[name] === typeof undefined) {
72
- console.log (`setting ${name} to global`)
72
+
73
73
  const getApp = () => {
74
74
  // if it hasne been intialized yet then do that
75
75
  if (!_app) {
76
+ console.log (`setting ${name} to global`)
76
77
  _app = {
77
78
  fetch
78
79
  }
@@ -1,18 +1,8 @@
1
- import sleepSynchronously from 'sleep-synchronously';
2
1
  import { Proxies } from '../../support/proxies.js'
3
- import { newBlob } from './fakeblob.js'
4
- import {Utils} from '../../support/utils.js'
5
- /**
6
- * a blocking sleep to emulate Apps Script
7
- * @param {number} ms number of milliseconds to sleep
8
- */
9
- const sleep = (ms) => {
10
- Utils.assert.number (ms, `Cannot convert ${ms} to int.`)
11
- sleepSynchronously(ms);
12
- }
2
+ import { newFakeUtilities } from './fakeutilities.js';
13
3
 
14
4
 
15
- // This will eventually hold a proxy for DriveApp
5
+ // This will eventually hold a proxy for Utilities
16
6
  let _app = null
17
7
 
18
8
  /**
@@ -20,19 +10,14 @@ let _app = null
20
10
  */
21
11
  const name = "Utilities"
22
12
  if (typeof globalThis[name] === typeof undefined) {
23
- console.log (`setting ${name} to global`)
24
13
  const getApp = () => {
25
- // if it hasne been intialized yet then do that
14
+ // if it hasnt been intialized yet then do that
26
15
  if (!_app) {
27
- _app = {
28
- sleep,
29
- newBlob
30
- }
16
+ console.log (`setting ${name} to global`)
17
+ _app = newFakeUtilities()
31
18
  }
32
19
  // this is the actual driveApp we'll return from the proxy
33
20
  return _app
34
21
  }
35
-
36
22
  Proxies.registerProxy (name, getApp)
37
-
38
23
  }
@@ -11,10 +11,10 @@ class FakeBlob {
11
11
  /**
12
12
  *
13
13
  * @constructor
14
- * @param {byte[]} [data] data
14
+ * @param {*} [data] data
15
15
  * @param {string} [contentType]
16
16
  * @param {string} [name]
17
- * @returns {FakeDriveFile}
17
+ * @returns {FakeBlob}
18
18
  */
19
19
  constructor(data, contentType, name) {
20
20
  this._data = Utils.settleAsBytes(data)
@@ -45,7 +45,7 @@ class FakeBlob {
45
45
  }
46
46
 
47
47
  copyBlob() {
48
- return newBlob(this.getBytes(), this.getContentType(), this.getName())
48
+ return newFakeBlob(this.getBytes(), this.getContentType(), this.getName())
49
49
  }
50
50
 
51
51
  setBytes(data) {
@@ -72,4 +72,4 @@ class FakeBlob {
72
72
  }
73
73
 
74
74
  }
75
- export const newBlob = (...args) => Proxies.guard(new FakeBlob(...args))
75
+ export const newFakeBlob = (...args) => Proxies.guard(new FakeBlob(...args))
@@ -0,0 +1,105 @@
1
+ import sleepSynchronously from 'sleep-synchronously';
2
+ import { Proxies } from '../../support/proxies.js'
3
+ import { newFakeBlob } from './fakeblob.js'
4
+ import { Utils } from '../../support/utils.js'
5
+ import { gzipType, zipType } from '../../support/constants.js'
6
+ import { randomUUID } from 'node:crypto'
7
+ import { gzipSync , gunzipSync} from 'node:zlib'
8
+ import { Syncit } from '../../support/syncit.js';
9
+
10
+ class FakeUtilities {
11
+ constructor() {
12
+
13
+ }
14
+ /**
15
+ * a blocking sleep to emulate Apps Script
16
+ * @param {number} ms number of milliseconds to sleep
17
+ */
18
+ sleep(ms) {
19
+ Utils.assert.number(ms, `Cannot convert ${ms} to int.`)
20
+ sleepSynchronously(ms);
21
+ }
22
+
23
+ /*
24
+ * @param {*} [data] data
25
+ * @param {string} [contentType]
26
+ * @param {string} [name]
27
+ * @returns {FakeBlob}
28
+ */
29
+ newBlob(data, contentType, name) {
30
+ return newFakeBlob(data, contentType, name)
31
+ }
32
+ /**
33
+ * gets a uid
34
+ * @returns {string}
35
+ */
36
+ getUuid() {
37
+ return randomUUID()
38
+ }
39
+ /**
40
+ * gzip-compresses the provided Blob data and returns it in a new Blob object.
41
+ * @param {FakeBlob} blob
42
+ * @param {string} [name]
43
+ * @returns {FakeBlob}
44
+ */
45
+ gzip(blob, name) {
46
+ // can set the name if required
47
+ const buffer = Buffer.from (blob.getBytes())
48
+ return this.newBlob (gzipSync(buffer), gzipType, (name || blob.getName() || 'archive.gz'))
49
+ }
50
+ /**
51
+ * Creates a new Blob object that is a zip file containing the data from the Blobs passed in.
52
+ * @param {FakeBlob[]} blobs
53
+ * @param {string} [name=archive.zip]
54
+ * @returns {FakeBlob}
55
+ */
56
+ zip(blobs, name = "archive.zip") {
57
+ // decided to use 'archiver' rather than zlib for this as the objective may be to create a file containing multiple files
58
+ // zlib only supports singe files
59
+ const zipped = Syncit.fxZipper ({blobs})
60
+ return newFakeBlob (zipped, zipType , name)
61
+ }
62
+
63
+ /**
64
+ * Takes a Blob representing a zip file and returns its component blobs.
65
+ * @param {FakeBlob} blob
66
+ * @param {string} [name]
67
+ * @returns {FakeBlob[]}
68
+ */
69
+ unzip (blob) {
70
+ const unzipped = Syncit.fxUnZipper ({blob})
71
+ // the content type is lost in a zipped file, same as Apps Script behavior - which seems to be to use the extension to reassert content type
72
+ return unzipped.map (f=> newFakeBlob (f.bytes, null, f.name)).map(f=>f.setContentTypeFromExtension())
73
+ }
74
+
75
+ /**
76
+ * Uncompresses a Blob object and returns a Blob containing the uncompressed data.
77
+ * @param {FakeBlob} blob
78
+ * @returns {FakeBlob}
79
+ */
80
+ ungzip(blob) {
81
+ const buffer = Buffer.from (blob.getBytes())
82
+ const name = blob.getName()
83
+ const newName = name ? name.replace(/\.gz$/,"") : null
84
+ return this.newBlob (gunzipSync(buffer), null, newName)
85
+ }
86
+
87
+ base64Encode (data, charset) {
88
+ return Buffer.from(Utils.settleAsBytes(data,charset)).toString('base64')
89
+ }
90
+
91
+ base64EncodeWebSafe (data, charset) {
92
+ return Buffer.from(Utils.settleAsBytes(data,charset)).toString('base64url')
93
+ }
94
+
95
+ base64Decode (b64) {
96
+ return Utils.settleAsBytes (Buffer.from (b64, 'base64'))
97
+ }
98
+
99
+ base64DecodeWebSafe (b64) {
100
+ return Utils.settleAsBytes (Buffer.from (b64, 'base64url'))
101
+ }
102
+
103
+ }
104
+
105
+ export const newFakeUtilities = () => Proxies.guard(new FakeUtilities())
@@ -1,5 +1,6 @@
1
1
  import { GoogleAuth } from 'google-auth-library'
2
2
  import is from '@sindresorhus/is'
3
+ import { createHash } from 'node:crypto'
3
4
 
4
5
  const _authScopes = new Set([])
5
6
 
@@ -8,8 +9,14 @@ let _auth = null
8
9
  let _projectId = null
9
10
  let _tokenInfo = null
10
11
  let _accessToken = null
12
+ let _manifest = null
13
+ let _clasp = null
11
14
 
12
15
  let _settings = null
16
+ const setManifest = (manifest) => _manifest = manifest
17
+ const setClasp = (clasp) => _clasp = clasp
18
+ const getManifest = () => _manifest
19
+ const getClasp = () => _clasp
13
20
  const getSettings = () => _settings
14
21
  const getScriptId = () => getSettings().scriptId
15
22
  const getDocumentId = () => getSettings().documentId
@@ -29,10 +36,10 @@ const getAccessToken= () => {
29
36
  return _accessToken
30
37
  }
31
38
 
32
-
39
+ const getTimeZone = () => getManifest().timeZone
33
40
  const getUserId = () => getTokenInfo().sub
34
41
  const getTokenScopes = () => getTokenInfo().scope
35
-
42
+ const getHashedUserId = () => createHash('md5').update(getUserId()+'hud').digest().toString('hex')
36
43
 
37
44
 
38
45
  /**
@@ -142,5 +149,12 @@ export const Auth = {
142
149
  getDocumentId,
143
150
  setSettings,
144
151
  getCachePath,
145
- getPropertiesPath
152
+ getPropertiesPath,
153
+ getTokenInfo,
154
+ getHashedUserId,
155
+ setManifest,
156
+ setClasp,
157
+ getManifest,
158
+ getClasp,
159
+ getTimeZone
146
160
  }
@@ -15,3 +15,8 @@ export const spreadsheetType = `${gooType}.spreadsheet`
15
15
 
16
16
  export const isGoogleType = (mimeType) =>
17
17
  mimeType && mimeType.substring(0,gooType.length) === gooType
18
+
19
+ export const gzipType = 'application/x-gzip'
20
+ export const zipType = 'application/zip'
21
+
22
+ export const notYetImplemented = `That is not yet implemented - watch https://github.com/brucemcpherson/gas-fakes for progress`
@@ -4,8 +4,8 @@
4
4
  import makeSynchronous from 'make-synchronous';
5
5
  import path from 'path'
6
6
  import { Auth } from "./auth.js"
7
-
8
-
7
+ import { randomUUID } from 'node:crypto'
8
+ import mime from 'mime';
9
9
 
10
10
  const authPath = "../support/auth.js"
11
11
  const drapisPath = "../services/drive/drapis.js"
@@ -31,37 +31,146 @@ const cacheDefaultPath = "/tmp/gas-fakes/cache"
31
31
  */
32
32
  const getModulePath = (relTarget) => path.resolve(import.meta.dirname, relTarget)
33
33
 
34
+ /**
35
+ * zipper
36
+ * @param {object} p
37
+ * @param {FakeBlob} p.blobs an array of blobs to be zipped
38
+ * @returns {FakeBlob} a combined zip file
39
+ */
40
+ const fxZipper = ({ blobs }) => {
41
+
42
+ const fx = makeSynchronous(async ({ blobsContent }) => {
43
+
44
+ const { default: archiver } = await import('archiver')
45
+ const { getStreamAsBuffer } = await import('get-stream')
46
+ const { PassThrough } = await import('node:stream')
47
+
48
+ const passthrough = new PassThrough()
49
+
50
+ // just use the default compression level
51
+
52
+ const archive = archiver.create('zip', {})
53
+
54
+ const doArchive = async () => {
55
+
56
+ // warning could be non destructive
57
+ archive.on("warning", function (err) {
58
+ if (err.code === "ENOENT") {
59
+ console.log("....warning on archiver", err)
60
+ } else {
61
+ // throw error
62
+ return Promise.reject(err)
63
+ }
64
+ });
65
+
66
+ archive.on("error", function (err) {
67
+ return Promise.reject(err)
68
+ })
69
+
70
+ const result = getStreamAsBuffer(archive.pipe(passthrough))
71
+
72
+ blobsContent.forEach(f => {
73
+ archive.append(Buffer.from(f.bytes), { name: f.name })
74
+ })
75
+
76
+ archive.finalize()
77
+
78
+ return result.then(buffer => Array.from(buffer))
79
+
80
+ }
81
+
82
+ return doArchive()
83
+ .catch(err => {
84
+ console.log('...archiver failed with error', err)
85
+ return Promise.reject(err)
86
+ })
87
+
88
+ })
89
+
90
+
91
+ const dupCheck = new Set()
92
+ const blobsContent = blobs.map((f,i) => {
93
+ const ext = mime.getExtension(f.getContentType())
94
+ const name = f.getName() || `Untitled${i+1}${ext ? "."+ext : ""}`
95
+ if (dupCheck.has (name)) {
96
+ throw new Error(`Duplicate filename ${name} not allowed in zip`)
97
+ }
98
+ dupCheck.add (name)
99
+ return {
100
+ name,
101
+ bytes: f.getBytes()
102
+ }
103
+ })
104
+ // check we don't have duplicate names
105
+
106
+
107
+ return fx({
108
+ blobsContent
109
+ })
110
+
111
+
112
+ }
34
113
 
114
+ /**
115
+ * Unzipper
116
+ * @param {object} p
117
+ * @param {FakeBlob} p.blob the blob containing the zipped files
118
+ * @returns {FakeBlob[]} each of the files unzipped
119
+ */
120
+ const fxUnZipper = ({ blob }) => {
121
+
122
+ const fx = makeSynchronous(async ({ blobContent }) => {
123
+
124
+ const { default: unzipper } = await import('unzipper')
125
+ const { getStreamAsBuffer } = await import('get-stream')
126
+
127
+ const buffer = Buffer.from(blobContent.bytes)
128
+ const unzipped = await unzipper.Open.buffer(buffer)
129
+
130
+ const result = await Promise.all(unzipped.files.map(async file => {
131
+ const bytes = await getStreamAsBuffer(file.stream())
132
+
133
+ return {
134
+ bytes,
135
+ name: file.path
136
+ }
137
+ }))
138
+
139
+ return result
140
+ })
141
+
142
+ const blobContent = {
143
+ name: blob.getName(),
144
+ bytes: blob.getBytes()
145
+ }
146
+
147
+
148
+ return fx({
149
+ blobContent
150
+ })
151
+
152
+
153
+ }
35
154
  /**
36
155
  * we dont want to generate a lot of async/sync calls so start by getting themanifest stuff out of the way
37
156
  * @param {string} [manifestPath ='./appsscript.json']
38
157
  *
39
158
  */
40
- const fxInit = ({
41
- manifestPath = manifestDefaultPath,
42
- claspPath = claspDefaultPath,
159
+ const fxInit = ({
160
+ manifestPath = manifestDefaultPath,
161
+ claspPath = claspDefaultPath,
43
162
  settingsPath = settingsDefaultPath,
44
163
  cachePath = cacheDefaultPath,
45
164
  propertiesPath = propertiesDefaultPath
46
165
  } = {}) => {
47
166
 
48
- const fx = makeSynchronous(async ({ manifestPath, authPath, claspPath, settingsPath, mainDir, cachePath, propertiesPath }) => {
167
+ const fx = makeSynchronous(async ({ manifestPath, authPath, claspPath, settingsPath, mainDir, cachePath, propertiesPath, fakeId }) => {
49
168
 
50
169
  // get the settings and manifest
51
170
  const path = await import('path')
52
171
  const { readFile, writeFile, mkdir } = await import('node:fs/promises')
53
172
  const { Auth } = await import(authPath)
54
173
 
55
- /// make a fake scriptid
56
- const makeFakeId = (length = 24) => {
57
- const seed = new Date().getTime()
58
- let t = ''
59
- while (t.length < length) {
60
- t += Math.trunc(seed * Math.random()).toString(36)
61
- }
62
- return t.substring(0, length)
63
- }
64
-
65
174
  // get a file and parse if it exists
66
175
  const getIfExists = async (file) => {
67
176
  try {
@@ -94,7 +203,7 @@ const fxInit = ({
94
203
  ])
95
204
 
96
205
  /// if we dont have a scriptId we need to check in clasp or make a fakeone
97
- settings.scriptId = settings.scriptId || clasp.scriptId || makeFakeId()
206
+ settings.scriptId = settings.scriptId || clasp.scriptId || fakeId
98
207
 
99
208
  // if we don't have a documentID, then see if this is a bound one
100
209
  settings.documentId = settings.documentId || null
@@ -103,12 +212,12 @@ const fxInit = ({
103
212
  settings.cache = settings.cache || cachePath
104
213
  settings.properties = settings.properties || propertiesPath
105
214
 
106
- console.log (`...cache will be in ${settings.cache}`)
107
- console.log (`...properties will be in ${settings.properties}`)
215
+ console.log(`...cache will be in ${settings.cache}`)
216
+ console.log(`...properties will be in ${settings.properties}`)
108
217
 
109
218
  // now update all that if anything has changed
110
- const strSet = JSON.stringify(settings,null,2)
111
- if (JSON.stringify(_settings,null, 2) !== strSet) {
219
+ const strSet = JSON.stringify(settings, null, 2)
220
+ if (JSON.stringify(_settings, null, 2) !== strSet) {
112
221
  await mkdir(settingsDir, { recursive: true })
113
222
  console.log('...writing to ', settingsFile)
114
223
  writeFile(settingsFile, strSet, { flag: 'w' })
@@ -140,7 +249,9 @@ const fxInit = ({
140
249
  projectId,
141
250
  tokenInfo,
142
251
  accessToken,
143
- settings
252
+ settings,
253
+ manifest,
254
+ clasp
144
255
  }
145
256
 
146
257
 
@@ -158,14 +269,17 @@ const fxInit = ({
158
269
  authPath: getModulePath(authPath),
159
270
  mainDir,
160
271
  cachePath,
161
- propertiesPath
272
+ propertiesPath,
273
+ fakeId: randomUUID()
162
274
  })
163
275
  const {
164
276
  scopes,
165
277
  projectId,
166
278
  tokenInfo,
167
279
  accessToken,
168
- settings
280
+ settings,
281
+ manifest,
282
+ clasp
169
283
  } = synced
170
284
 
171
285
  // set these values from the subprocess for the main project
@@ -174,6 +288,8 @@ const fxInit = ({
174
288
  Auth.setTokenInfo(tokenInfo)
175
289
  Auth.setAccessToken(accessToken)
176
290
  Auth.setSettings(settings)
291
+ Auth.setClasp(clasp)
292
+ Auth.setManifest(manifest)
177
293
  return synced
178
294
 
179
295
  }
@@ -363,5 +479,7 @@ export const Syncit = {
363
479
  fxDrive,
364
480
  fxDriveMedia,
365
481
  fxInit,
366
- fxStore
482
+ fxStore,
483
+ fxZipper,
484
+ fxUnZipper
367
485
  }