@mcpher/gas-fakes 1.0.5 → 1.0.6

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/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "pub": "npm publish --access public"
22
22
  },
23
23
  "name": "@mcpher/gas-fakes",
24
- "version": "1.0.5",
24
+ "version": "1.0.6",
25
25
  "main": "main.js",
26
26
  "description": "A proof of concept implementation of Apps Script Environment on Node",
27
27
  "repository": "github:brucemcpherson/gas-fakes",
@@ -1,9 +1,12 @@
1
1
  import { Proxies } from '../../support/proxies.js'
2
- import { notYetImplemented, isGood, throwResponse, minFields } from '../../support/helpers.js'
2
+ import { notYetImplemented, isGood, throwResponse, minFields, isFolder } from '../../support/helpers.js'
3
3
  import { Syncit } from '../../support/syncit.js'
4
- import { getFromFileCache, improveFileCache, checkResponse } from '../../support/filecache.js';
5
- import {Utils} from '../../support/utils.js'
6
- const {is} = Utils
4
+ import { improveFileCache, checkResponse } from '../../support/filecache.js';
5
+ import { Utils, mergeParamStrings } from '../../support/utils.js'
6
+
7
+ const { is } = Utils
8
+
9
+ const apiProp = 'files'
7
10
 
8
11
  /**
9
12
  * these apply to Drive.files
@@ -12,7 +15,7 @@ class FakeAdvDriveFiles {
12
15
  constructor(drive) {
13
16
  this.drive = drive
14
17
  this.name = 'Drive.Files'
15
- this.apiProp = 'files'
18
+ this.apiProp = apiProp
16
19
  }
17
20
 
18
21
  toString() {
@@ -27,24 +30,14 @@ class FakeAdvDriveFiles {
27
30
  return notYetImplemented()
28
31
  }
29
32
 
30
- update() {
31
- return notYetImplemented()
32
- }
33
+
33
34
 
34
35
  list(params = {}) {
35
36
  // this is pretty straightforward as the onus is on thecaller to provide a valid queryOb
36
37
  // and validation will be done by the api.
37
38
  // however to support caching, we'll fiddle with the fields parameter
38
- const fob = params.fields && params.fields.files
39
- if (fob) {
40
- const far = tidyFieldsFar(fob)
41
- params = {
42
- ...params, fields: {
43
- ...params.fields,
44
- files: far.join(",")
45
- }
46
- }
47
- }
39
+
40
+ params.fields = mergeParamStrings(params.fields || "", `files(${minFields})`)
48
41
 
49
42
  // sincify that call
50
43
  const { response, data } = Syncit.fxDrive({ prop: this.apiProp, method: 'list', params })
@@ -55,8 +48,11 @@ class FakeAdvDriveFiles {
55
48
  }
56
49
 
57
50
  // lets improve cache with any enhanced data we've found
51
+ /// extract out the fieldslist
52
+ const fields = params.fields.replace(/.*files\(([^)]*)\).*/,"$1")
53
+
58
54
  data.files.forEach(f => {
59
- improveFileCache(f.id, f)
55
+ improveFileCache(f.id, f,fields)
60
56
  })
61
57
  return data
62
58
 
@@ -117,70 +113,70 @@ class FakeAdvDriveFiles {
117
113
  * @returns {Drive.File}
118
114
  */
119
115
  get(id, params = {}, { allow404 = true } = {}) {
120
-
121
- // now clean up
122
- let far = tidyFieldsFar(params)
123
-
124
- // the cache will check the fields it already has against those requested
125
- const { cachedFile, good } = getFromFileCache(id, far)
126
- if (good) return cachedFile
127
-
128
-
129
- // we'll enhance the cache with the current value of any already fetched field by fetching it again
130
- params = { ...params, fileId: id, fields: enhanceFar({ cachedFile, far }) }
131
-
132
- // run as a sub process to unasync it
133
- const { response, data: file } = Syncit.fxDrive({ prop: this.apiProp, method: 'get', params })
134
-
135
- // maybe we need to throw an error
136
- checkResponse(id, response, allow404)
137
-
138
- // finally register in cache for next time
139
- return improveFileCache(id, file)
116
+ const {data} = Syncit.fxDriveGet ({ id, params, allow404, allowCache: true })
117
+ return data
140
118
  }
141
119
 
142
120
  /**
143
121
  * ceate a file and optionally upload some data
144
122
  * @param {File} [file] file resource
145
123
  * @param {Blob} [blob] the mediadata
124
+ * @param {string} [fields] (not and advanced drive option but allow me to use this from driveapp and avoid an extra fetch)
146
125
  */
147
- create(file = {}, blob) {
126
+ create(file = {}, blob, fields,params) {
127
+
148
128
  if (!is.undefined(blob) && !Utils.isBlob(blob)) {
149
129
  throw new Error("The mediaData parameter only supports Blob types for upload.")
150
130
  }
151
- const mimeType = file.mimeType || blob?.getContentType()
152
- const name = file.name || blob?.getName() || (isFolder({ mimeType }) ? "New Folder" : "Untitled")
153
- const result = Syncit.fxStreamUpMedia({ fields: minFields, blob, file: { ...file, mimeType, name } })
154
- const { data, response } = result
155
- checkResponse(data?.id, response, false)
156
- improveFileCache(data.id, data)
157
- return data
131
+
132
+ // must have some kind of name so derive if not given
133
+ const name = file.name || blob?.getName() || (isFolder(file) ? "New Folder" : "Untitled")
134
+ return updateOrCreate ({method: "create", file: { ...file, name }, blob, fields, params })
135
+
158
136
  }
159
137
 
160
138
  generateIds() {
161
139
  return notYetImplemented()
162
140
  }
163
141
 
142
+ /**
143
+ * patch a file and ioptionally upoad new content
144
+ * @param {Drive.File} file the resource metadata
145
+ * @param {string} fileId the fileid to update
146
+ * @param {FakeBlob} [blob] new media if required
147
+ * @param {string} [fields] (not and advanced drive option but allow me to use this from driveapp and avoid an extra fetch)
148
+ * @param {object} [params] any extra params
149
+ * @returns {Drive.File} updated
150
+ */
151
+ update(file, fileId, blob, fields,params) {
152
+ if (!is.nonEmptyString(fileId)) {
153
+ throw new Error(`API call to drive.files.update failed with error: Required`)
154
+ }
155
+ return updateOrCreate ({method: 'update', file, blob, fileId , fields, params})
156
+ }
164
157
  /**
165
158
  * ceate a file and optionally upload some data
166
159
  * @param {File} [file] file resource to patch
167
160
  * @param {string} fileId the file to copy
168
161
  * @param {object} [options] request options
169
162
  */
170
- copy(file, fileId, options) {
163
+ copy(file, fileId, options={}) {
164
+
171
165
  if (!is.nonEmptyString(fileId)) {
172
- throw new Error ("API call to drive.files.copy failed with error: Required")
166
+ throw new Error(`API call to drive.files.copy failed with error: Required`)
173
167
  }
168
+ const fields = mergeParamStrings(options.fields || "",minFields)
174
169
  const params = {
175
- fields: minFields,
170
+ fields,
176
171
  fileId,
177
172
  resource: file
178
173
  }
179
174
 
180
- const { response, data } = Syncit.fxDrive({ prop: this.apiProp, method: 'copy', params, options })
175
+ const { response, data } = Syncit.fxDrive({ prop: apiProp, method: 'copy', params, options })
181
176
  checkResponse(data?.id, response, false)
182
- improveFileCache(data.id, data)
177
+ improveFileCache(data.id, data,fields)
183
178
  return data
179
+
184
180
  }
185
181
 
186
182
  export() {
@@ -224,4 +220,25 @@ const enhanceFar = ({ cachedFile, far }) => {
224
220
  return far.join(",")
225
221
  }
226
222
 
223
+
224
+ /**
225
+ * ceate/patch a file and optionally upload some data
226
+ * update and copy are virtually the same payload
227
+ * @param {string} method the api method
228
+ * @param {File} [file] file resource to patch/create
229
+ * @param {string} fileId the file to update
230
+ * @param {FakeBlob} [blob] blob if media is provided
231
+ */
232
+ const updateOrCreate = ( {method, file = {}, blob, fileId, fields="" , params}) => {
233
+
234
+ if (!Utils.isNU(blob) && !Utils.isBlob(blob)) {
235
+ throw new Error("The mediaData parameter only supports Blob types for upload.")
236
+ }
237
+ fields = mergeParamStrings(fields, minFields)
238
+ // streamupmedia takes care of improving the cache
239
+ const result = Syncit.fxStreamUpMedia({ method, fields, blob, file , fileId, params })
240
+ const { data } = result
241
+ return data
242
+ }
243
+
227
244
  export const newFakeAdvDriveFiles = (...args) => Proxies.guard(new FakeAdvDriveFiles(...args))
@@ -1,6 +1,6 @@
1
1
  import { FakeDriveFolder, newFakeDriveFolder } from './fakedrivefolder.js'
2
- import { newFakeFolderApp } from './fakefolderapp.js'
3
2
  import { newFakeDriveFile } from './fakedrivefile.js'
3
+ import { newFakeFolderApp } from './fakefolderapp.js'
4
4
  import { notYetImplemented, isFolder } from '../../support/helpers.js'
5
5
  import { Proxies } from '../../support/proxies.js'
6
6
  /**
@@ -12,7 +12,6 @@ export class FakeDriveApp {
12
12
 
13
13
  constructor() {
14
14
  const rf = Drive.Files.get('root', {}, { allow404: true })
15
- // because the parent folder prop isnt returned we'll spoof it
16
15
  this.__rootFolder = newFakeDriveFolder(rf)
17
16
  this.folderApp = newFakeFolderApp()
18
17
  this.__settleClass = (file) => isFolder(file) ? newFakeDriveFolder(file) : newFakeDriveFile(file)
@@ -1,9 +1,12 @@
1
1
  import { FakeDriveMeta } from "./fakedrivemeta.js"
2
2
  import { Proxies } from '../../support/proxies.js'
3
- import { isFolder } from '../../support/helpers.js'
3
+ import { isFolder, isFakeFolder, argsMatchThrow } from '../../support/helpers.js'
4
4
  import { Syncit } from "../../support/syncit.js"
5
5
  import { FakeDriveFolder } from "./fakedrivefolder.js"
6
6
  import { Utils } from "../../support/utils.js"
7
+ import { settleAsBlob } from "../utilities/fakeblob.js"
8
+ import { improveFileCache } from "../../support/filecache.js"
9
+
7
10
  const { is } = Utils
8
11
 
9
12
  /**
@@ -65,48 +68,75 @@ class FakeDriveFile extends FakeDriveMeta {
65
68
  return this.__getDecorated("webContentLink")
66
69
  }
67
70
 
71
+ /**
72
+ * set the content to something else
73
+ * @param {string} content apparently this can only be a string and not a blob
74
+ * @return {FakeDriveFile} self
75
+ */
76
+ setContent(content) {
77
+ // for param checking
78
+ const matchThrow = () => argsMatchThrow(Array.from(arguments))
79
+ // apps script does a toString on the arg rather than failing
80
+ if (!is.function (content?.toString)) {
81
+ matchThrow()
82
+ }
83
+
84
+ // this remains its current mimetype even though its now text
85
+ const blob = settleAsBlob(content.toString(), this.getMimeType(), this.getName())
86
+ const data = Drive.Files.update({}, this.getId(), blob)
87
+
88
+ // merge this with already known fields and improve cache
89
+ this.meta = {...this.meta, ...data}
90
+ improveFileCache(this.getId(), this.meta)
91
+ return this
92
+
93
+ }
94
+
68
95
  /**
69
96
  * make a copy
70
97
  * @param {FakeDriveFolder|string|null} [destinationOrName] where to copy it to/chaneg the name if required
71
98
  * @param {FakeDriveFolder} [destination] where to copy it to
72
99
  */
73
100
  makeCopy(destinationOrName, destination) {
74
-
101
+
75
102
  // default is same name as copied file
76
103
  let name = this.__getDecorated("name")
77
104
 
78
105
  // default is the parent of the file to be copied
79
106
  let parents = this.__getDecorated("parents")
80
- const nargs = arguments.length
81
107
 
82
108
  // for param checking
83
- const metaFolder = (item) => isFolder(item?.meta) ? "DriveApp.Folder" : is(item)
84
- const passedTypes = [metaFolder(destinationOrName), metaFolder(destination)]
85
- .slice(0, nargs).map(Utils.capital).join(",")
86
- const matchThrow = (mess = "") => {
87
- throw new Error(`The parameters (${passedTypes}) don't match the method ${mess}`)
88
- }
109
+ const matchThrow = () => argsMatchThrow(Array.from(arguments))
110
+
111
+
112
+ // cant move the root folder
113
+ this.__preventRootDamage("copy", this)
89
114
 
90
115
  // check args make sense
91
116
  if (Utils.isNU(destination) && Utils.isNU(destinationOrName)) {
92
117
  // makecopy()
93
118
  // no args provided, we use the defaults
94
- } else if (isFolder(destinationOrName?.meta)) {
119
+
120
+ } else if (isFakeFolder(destinationOrName)) {
95
121
  // makecopy (afolder)
96
122
  // destination is a folder, so no 2nd arg required
97
123
  parents = [destinationOrName.__getDecorated("id")]
98
124
  if (!Utils.isNU(destination)) matchThrow()
125
+
99
126
  } else if (!is.nonEmptyString(destinationOrName)) {
100
127
  // makecopy (notastring,...)
101
128
  // they tried to give a name but its not a string
102
129
  matchThrow()
103
- } else if (isFolder(destination?.meta)) {
130
+
131
+ } else if (isFakeFolder(destination)) {
104
132
  // makecopy (a string,a folder)
105
133
  name = destinationOrName
106
134
  parents = [destination.__getDecorated("id")]
135
+
107
136
  } else if (Utils.isNU(destination)) {
108
137
  // makecopy (string)
109
138
  name = destinationOrName
139
+
110
140
  } else {
111
141
  // makecopy (string, notafolder)
112
142
  matchThrow()
@@ -72,7 +72,7 @@ export const getFilesIterator = ({
72
72
  assert.boolean(folderTypes)
73
73
  assert.boolean(fileTypes)
74
74
 
75
- // DriveApp doesnt give option to specify these so this will be fixes
75
+ // DriveApp doesnt give option to specify these so this will be fixed
76
76
  const fields = `files(${minFields}),nextPageToken`
77
77
 
78
78
  /**
@@ -97,6 +97,8 @@ export const getFilesIterator = ({
97
97
 
98
98
 
99
99
  // format the results into the folder or file object
100
+ assert.array (data.files)
101
+ assert.function (DriveApp.__settleClass)
100
102
  tank = data.files.map(DriveApp.__settleClass)
101
103
 
102
104
  }
@@ -130,12 +132,16 @@ export const getParentsIterator = ({
130
132
  }) => {
131
133
  const { assert } = Utils
132
134
  assert.object(file)
133
- assert.array(file.parents)
135
+ // if its rott folder can be null
136
+ const parents = is.null (file.parents) ? [] : file.parents
137
+ assert.array(parents)
134
138
 
135
139
  function* filesink() {
136
- // the result tank, we just get them all by id
137
- let tank = file.parents.map(id => Drive.Files.get(id, {}, { allow404: false }))
140
+ // the result tank, we just get them all by id - will return the usual minfields
141
+ // and will also stick them in cache
142
+ let tank = parents.map(id => Drive.Files.get(id, {}, { allow404: false }))
138
143
 
144
+ // let them out, 1 at a time
139
145
  while (tank.length) {
140
146
  yield DriveApp.__settleClass(tank.splice(0, 1)[0])
141
147
  }
@@ -186,6 +192,7 @@ const fileLister = ({
186
192
  params.pageToken = pageToken
187
193
  }
188
194
 
195
+
189
196
  // this will have be synced from async
190
197
  try {
191
198
  const result = Drive.Files.list(params)
@@ -9,12 +9,15 @@
9
9
  */
10
10
 
11
11
  import is from '@sindresorhus/is';
12
- import { notYetImplemented } from '../../support/helpers.js'
13
- import { getParentsIterator, getPermissionIterator} from './fakedriveiterators.js';
12
+ import { isFolder, notYetImplemented, argsMatchThrow, isFakeFolder, minFields } from '../../support/helpers.js'
13
+ import { getParentsIterator, getPermissionIterator } from './fakedriveiterators.js';
14
14
  import { newFakeUser } from '../session/fakeuser.js';
15
+ import { improveFileCache } from "../../support/filecache.js"
16
+
15
17
 
16
18
  /**
17
19
  * basic fake File meta data
20
+ * these are shared between folders and files
18
21
  * @class FakeDriveMeta
19
22
  * @returns {FakeDriveMeta}
20
23
  */
@@ -27,9 +30,19 @@ export class FakeDriveMeta {
27
30
  */
28
31
  constructor(meta) {
29
32
  this.meta = meta
30
-
33
+ this.__gas_fake_service = "DriveApp"
31
34
  }
32
35
 
36
+ __preventRootDamage = (operation) => {
37
+ if (this.__isRoot) {
38
+ console.log (`Can't do ${operation} on root folder`)
39
+ throw new Error("Access denied: DriveApp")
40
+ }
41
+ }
42
+ get __isRoot () {
43
+ const parents = this.__getDecorated("parents")
44
+ return is.null (parents)
45
+ }
33
46
  /**
34
47
  * for enhancing the file with fields not retrieved by default
35
48
  * @param {string} fields='' the required fields
@@ -41,7 +54,6 @@ export class FakeDriveMeta {
41
54
  throw new Error('decorate fields was not a non empty string')
42
55
  }
43
56
  const sf = fields.split(",")
44
-
45
57
  if (sf.every(f => Reflect.has(this.meta, f))) {
46
58
  return this
47
59
  }
@@ -49,9 +61,17 @@ export class FakeDriveMeta {
49
61
  const newMeta = Drive.Files.get(this.getId(), { fields }, { allow404: false })
50
62
  // need to merge this with already known fields
51
63
  this.meta = { ...this.meta, ...newMeta }
64
+ improveFileCache (this.getId(), this.meta, fields)
52
65
  return this
53
66
  }
54
67
 
68
+ /**
69
+ * this will return the type DriveApp.Folder or DriveApp.File
70
+ * return {string}
71
+ */
72
+ __getFakeType() {
73
+ return isFolder(this.meta) ? "DriveApp.Folder" : "DriveApp.File"
74
+ }
55
75
  /**
56
76
  * the meta data for the following fields are not fetched by default
57
77
  */
@@ -59,6 +79,33 @@ export class FakeDriveMeta {
59
79
  return this.__decorateWithFields(prop).meta[prop]
60
80
  }
61
81
 
82
+ /**
83
+ * __updateMeta - used to set simple felds using update
84
+ * @param {string} prop the meta data to set
85
+ * @param {*} value what to set it to
86
+ * @param {string} type whhat type it should be
87
+ * @param {object} args array like item with what was passed to the original function
88
+ * @returns this self
89
+ */
90
+ __updateMeta(prop, value, type, ...args) {
91
+
92
+ // cant update any meta on root folder
93
+ this.__preventRootDamage (`set ${prop}`)
94
+
95
+ const matchThrow = () => argsMatchThrow(args)
96
+ if (!is[type](value)) {
97
+ matchThrow()
98
+ }
99
+ const file = {}
100
+ file[prop] = value
101
+
102
+ const data = Drive.Files.update(file, this.getId(), null, prop)
103
+ this.meta = {...this.meta, ...data}
104
+ improveFileCache (this.getId(), data)
105
+
106
+ return this
107
+ }
108
+
62
109
  // shared between folder and file
63
110
  toString() {
64
111
  return this.getName()
@@ -144,34 +191,116 @@ export class FakeDriveMeta {
144
191
  return getSharers(this.getId(), 'writer')
145
192
  }
146
193
 
194
+ /**
195
+ * get the file url
196
+ * @returns {string} the webviewlink
197
+ */
147
198
  getUrl() {
148
199
  return this.__getDecorated("webViewLink")
149
200
  }
150
201
 
202
+ /**
203
+ * moves a file to a mew destination
204
+ * @param {FakeDriveFolder} destination
205
+ * @returns self for chaining
206
+ */
207
+ moveTo(destination) {
208
+ // prepare for any arg errors
209
+ const matchThrow = () => argsMatchThrow(Array.from(arguments))
210
+ if (!isFakeFolder(destination)) {
211
+ matchThrow()
212
+ }
213
+ // pick up parents for destination if not already known
214
+ const newParent = destination.getId()
215
+ if (!is.nonEmptyString(newParent)) {
216
+ throw new Error (`expected to find destination id as a string but got ${newParent}`)
217
+ }
218
+
219
+ // cant move the root folder
220
+ this.__preventRootDamage("move")
151
221
 
222
+ // we cant just fix the resource parents, we have to add and remove
223
+ // so that's a 2 step
224
+ const params = {
225
+ addParents: newParent,
226
+ removeParents: this.__getDecorated("parents")[0]
227
+ }
152
228
 
153
- // TODO-----------
229
+ // need to make sure we get the new parents field back to improve cache with
230
+ const data = Drive.Files.update({}, this.getId(), null, "parents", params)
231
+
232
+ // merge this with already known fields and improve cache
233
+ this.meta = { ...this.meta, ...data }
234
+
235
+ improveFileCache(this.getId(), this.meta)
236
+ return this
237
+
238
+ }
239
+
240
+ /**
241
+ * @param {string} value the updated value
242
+ * @returns {FakeDriveFile|FakeDriveFolder} this self
243
+ */
244
+ setDescription(value) {
245
+ return this.__updateMeta("description", value, "string", arguments)
246
+ }
247
+
248
+ /**
249
+ * @param {string} value the updated value
250
+ * @returns {FakeDriveFile|FakeDriveFolder} this self
251
+ */
252
+ setName(value) {
253
+ return this.__updateMeta("name", value, "string", arguments)
254
+ }
255
+
256
+ /**
257
+ * Sets whether users with edit permissions to the Folder are allowed to share with other users or change the permissions.
258
+ * @param {boolean} value the updated value
259
+ * @returns {FakeDriveFile|FakeDriveFolder} this self
260
+ */
261
+ setShareableByEditors(value) {
262
+ return this.__updateMeta("writersCanShare", value, "boolean", arguments)
263
+ }
264
+
265
+ /**
266
+ * Determines whether users with edit permissions to the Folder/File are allowed to share with other users or change the permissions
267
+ * @returns {Boolean}
268
+ */
269
+ isShareableByEditors() {
270
+ return this.__getDecorated("writersCanShare")
271
+ }
154
272
 
155
- setDescription() {
156
- return notYetImplemented('setDescription')
273
+ /**
274
+ * Sets whether the Folder/File is starred in the user's Drive.
275
+ * @param {boolean} value the updated value
276
+ * @returns {FakeDriveFile|FakeDriveFolder} this self
277
+ */
278
+ setStarred(value) {
279
+ return this.__updateMeta("starred", value, "boolean", arguments)
157
280
  }
158
281
 
282
+ /**
283
+ * Whether the file has been trashed, either explicitly or from a trashed parent folder
284
+ * @param {boolean} value the updated value
285
+ * @returns {FakeDriveFile|FakeDriveFolder} this self
286
+ */
287
+ setTrashed(value) {
288
+ this.__preventRootDamage("trash")
289
+ return this.__updateMeta("trashed", value, "boolean", arguments)
290
+ }
159
291
 
292
+ // TODO-----------
160
293
 
161
294
  getSharingPermission() {
162
295
  return notYetImplemented('getSharingPermission')
163
296
  }
164
- setTrashed() {
165
- return notYetImplemented('setTrashed')
166
- }
297
+
167
298
 
168
299
  getSharingAccess() {
169
300
  return notYetImplemented('getSharingAccess')
170
301
  }
171
302
 
172
- setStarred() {
173
- return notYetImplemented('setStarred')
174
- }
303
+
175
304
  getResourceKey() {
176
305
  return notYetImplemented('getResourceKey')
177
306
  }
@@ -191,24 +320,13 @@ export class FakeDriveMeta {
191
320
  getAccess() {
192
321
  return notYetImplemented('getAccess')
193
322
  }
194
- moveTo() {
195
- return notYetImplemented('moveTo')
196
- }
197
-
198
- setName() {
199
- return notYetImplemented('setName')
200
- }
201
323
 
202
324
 
203
325
  revokePermissions() {
204
326
  return notYetImplemented('revokePermissions')
205
327
  }
206
- isShareableByEditors() {
207
- return notYetImplemented('isShareableByEditors')
208
- }
209
- setShareableByEditors() {
210
- return notYetImplemented('setShareableByEditors')
211
- }
328
+
329
+
212
330
  setOwner() {
213
331
  return notYetImplemented('setOwner')
214
332
  }
@@ -39,6 +39,10 @@ class FakeBlob {
39
39
  this._name = name || null
40
40
  }
41
41
 
42
+ toString () {
43
+ // oddly this just refurns the object name rather than converting content to string
44
+ return 'Blob'
45
+ }
42
46
 
43
47
  getBytes() {
44
48
  return this._data
@@ -67,7 +67,7 @@ class FakeUtilities {
67
67
  * @returns {FakeBlob[]}
68
68
  */
69
69
  unzip (blob) {
70
- const unzipped = Syncit.fxUnZipper ({blob})
70
+ const unzipped = Syncit.fxUnzipper ({blob})
71
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
72
  return unzipped.map (f=> newFakeBlob (f.bytes, null, f.name)).map(f=>f.setContentTypeFromExtension())
73
73
  }