@mcpher/gas-fakes 1.0.0
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/LICENSE +21 -0
- package/README.md +305 -0
- package/main.js +1 -0
- package/package.json +23 -0
- package/src/index.js +4 -0
- package/src/services/drive/app.js +31 -0
- package/src/services/drive/drapis.js +23 -0
- package/src/services/drive/fakedrive.js +568 -0
- package/src/services/drive/fakedrivehelpers.js +141 -0
- package/src/services/scriptapp/app.js +133 -0
- package/src/services/urlfetchapp/app.js +128 -0
- package/src/services/utilities/app.js +37 -0
- package/src/services/utilities/fakeblob.js +73 -0
- package/src/support/auth.js +154 -0
- package/src/support/constants.js +16 -0
- package/src/support/peeker.js +44 -0
- package/src/support/proxies.js +74 -0
- package/src/support/syncit.js +235 -0
- package/src/support/utils.js +144 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js'
|
|
2
|
+
import { newPeeker } from '../../support/peeker.js'
|
|
3
|
+
import { Utils } from '../../support/utils.js'
|
|
4
|
+
import {
|
|
5
|
+
handleError,
|
|
6
|
+
decorateParams,
|
|
7
|
+
minFields,
|
|
8
|
+
minFieldsList,
|
|
9
|
+
isFolder,
|
|
10
|
+
throwResponse
|
|
11
|
+
} from './fakedrivehelpers.js'
|
|
12
|
+
import { Syncit } from '../../support/syncit.js'
|
|
13
|
+
import { folderType } from '../../support/constants.js'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* things are pretty slow on node, especially repeatedly getting parents
|
|
18
|
+
* so we'll cache that over here
|
|
19
|
+
*/
|
|
20
|
+
const CACHE_ENABLED = true
|
|
21
|
+
const fileCache = new Map()
|
|
22
|
+
const getFromCache = (id) => {
|
|
23
|
+
if (CACHE_ENABLED) {
|
|
24
|
+
const file = fileCache.get(id)
|
|
25
|
+
if (file) return file
|
|
26
|
+
}
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* create a new drive file instance
|
|
32
|
+
* @param {...any} args
|
|
33
|
+
* @returns {FakeDriveFile}
|
|
34
|
+
*/
|
|
35
|
+
const newFakeDriveFile = (...args) => {
|
|
36
|
+
return Proxies.guard(new FakeDriveFile(...args))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* create a new drive folder instance
|
|
41
|
+
* @param {...any} args
|
|
42
|
+
* @returns {FakeDriveFolder}
|
|
43
|
+
*/
|
|
44
|
+
const newFakeDriveFolder = (...args) => {
|
|
45
|
+
return Proxies.guard(new FakeDriveFolder(...args))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* create a new drive app instance
|
|
50
|
+
* @param {...any} args
|
|
51
|
+
* @returns {FakeDriveApp}
|
|
52
|
+
*/
|
|
53
|
+
export const newFakeDriveApp = (...args) => {
|
|
54
|
+
return Proxies.guard(new FakeDriveApp(...args))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* list get any kind using the NODE client
|
|
60
|
+
* @param {string} [parentId] the parent id
|
|
61
|
+
* @param {function} [handler = handleError]
|
|
62
|
+
* @param {object|[object]} [qob] any additional queries
|
|
63
|
+
* @param {TODO} [fields] the fields to fetch
|
|
64
|
+
* @param {TODO} [options] mimic fetchapp options
|
|
65
|
+
* @param {string} [pageToken=null] if we're doing a pagetoken kind of thing
|
|
66
|
+
* @param {boolean} folderTypes whether to get foldertypes
|
|
67
|
+
* @param {boolean} fileTypes whether to get fileTypes
|
|
68
|
+
* @returns {object} a collection of files {response, data}
|
|
69
|
+
*/
|
|
70
|
+
const fileLister = ({
|
|
71
|
+
qob, parentId, fields, handler, folderTypes, fileTypes, pageToken = null, fileName
|
|
72
|
+
}) => {
|
|
73
|
+
// enhance any already supplied query params
|
|
74
|
+
qob = Utils.arrify(qob) || []
|
|
75
|
+
if (parentId) {
|
|
76
|
+
qob.push(`'${parentId}' in parents`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// wheteher we're getting files,folders or both
|
|
80
|
+
if (!(folderTypes || fileTypes)) {
|
|
81
|
+
throw new Error(`Must specify either folder type,file type or both`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// exclusive xor - if they're both true we dont need to do any extra q filtering
|
|
85
|
+
if (folderTypes !== fileTypes) {
|
|
86
|
+
qob.push(`mimeType${fileTypes ? "!" : ""}='${folderType}'`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const q = qob.map(f => `(${f})`).join(" and ")
|
|
90
|
+
let params = { q }
|
|
91
|
+
if (pageToken) {
|
|
92
|
+
params.pageToken = pageToken
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
params = decorateParams({ fields, params, min: minFieldsList })
|
|
96
|
+
|
|
97
|
+
// this will have be synced from async
|
|
98
|
+
try {
|
|
99
|
+
const result = Syncit.fxDrive({ prop: "files", method: "list", params })
|
|
100
|
+
return result
|
|
101
|
+
} catch (err) {
|
|
102
|
+
handler(err)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* general getter by id
|
|
109
|
+
* @param {object} p args
|
|
110
|
+
* @param {string} p.id the id to get
|
|
111
|
+
* @param {boolean} [p.allow404=true] normally a 404 doesnt throw an error
|
|
112
|
+
* @returns {FakeDriveFolder}
|
|
113
|
+
*/
|
|
114
|
+
const getFileById = ({ id, allow404 = true, fields }) => {
|
|
115
|
+
|
|
116
|
+
// we'll pull this from cache if poss
|
|
117
|
+
const cachedFile = getFromCache(id)
|
|
118
|
+
if (cachedFile && (!fields || Utils.arrify(fields).every(f => Reflect.has(cachedFile, f)))) {
|
|
119
|
+
return cachedFile
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// it wasnt in cache
|
|
123
|
+
const { file, response } = fetchFile({ id, fields })
|
|
124
|
+
if (!file) {
|
|
125
|
+
if (!allow404) {
|
|
126
|
+
throwResponse(response)
|
|
127
|
+
} else {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (CACHE_ENABLED) {
|
|
132
|
+
fileCache.set(id, file)
|
|
133
|
+
}
|
|
134
|
+
return file
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* shared get any kind of file meta data by its id
|
|
140
|
+
* @param {string} [parentId] the parent id
|
|
141
|
+
* @param {function} [handler = handleError]
|
|
142
|
+
* @param {object[]} [qob] any additional queries
|
|
143
|
+
* @param {object|object[]} [fields] the fields to fetch
|
|
144
|
+
* @param {TODO} [options] mimic fetchapp options
|
|
145
|
+
* @param {boolean} folderTypes whether to get foldertypes
|
|
146
|
+
* @param {boolean} fileTypes whether to get fileTypes
|
|
147
|
+
* @returns {object} {Peeker}
|
|
148
|
+
*/
|
|
149
|
+
const getFilesIterator = ({
|
|
150
|
+
qob,
|
|
151
|
+
parentId = null,
|
|
152
|
+
fields = [],
|
|
153
|
+
handler = handleError,
|
|
154
|
+
folderTypes,
|
|
155
|
+
fileTypes
|
|
156
|
+
}) => {
|
|
157
|
+
|
|
158
|
+
// parentId can be null to search everywhere
|
|
159
|
+
if (!Utils.isNull(parentId)) Utils.assertType(parentId, "string")
|
|
160
|
+
Utils.assertType(handler, "function")
|
|
161
|
+
Utils.assertType(fields, "object")
|
|
162
|
+
Utils.assertType(folderTypes, "boolean")
|
|
163
|
+
Utils.assertType(fileTypes, "boolean")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* this generator will get chunks of matching files from the drive api
|
|
168
|
+
* and yield them 1 by 1 and handle paging if required
|
|
169
|
+
*/
|
|
170
|
+
function* filesink() {
|
|
171
|
+
// the result tank
|
|
172
|
+
let tank = []
|
|
173
|
+
// the next page token
|
|
174
|
+
let pageToken = null
|
|
175
|
+
|
|
176
|
+
do {
|
|
177
|
+
// if nothing in the tank, fill it up
|
|
178
|
+
if (!tank.length) {
|
|
179
|
+
|
|
180
|
+
const result = fileLister({
|
|
181
|
+
qob, parentId, fields, handler, folderTypes, fileTypes, pageToken
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// the presence of a nextPageToken is the signal that there's more to come
|
|
185
|
+
pageToken = result.data.nextPageToken
|
|
186
|
+
|
|
187
|
+
// format the results into the folder or file object
|
|
188
|
+
tank = result.data.files.map(
|
|
189
|
+
f => isFolder(f) ? newFakeDriveFolder(f) : newFakeDriveFile(f)
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// if we've got anything in the tank send back the oldest one
|
|
194
|
+
if (tank.length) {
|
|
195
|
+
yield tank.splice(0, 1)[0]
|
|
196
|
+
}
|
|
197
|
+
// if there's still anything left in the tank,
|
|
198
|
+
// or there's a page token to get more continue
|
|
199
|
+
} while (pageToken || tank.length)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// create the iterator
|
|
203
|
+
const fileit = filesink()
|
|
204
|
+
|
|
205
|
+
// a regular iterator doesnt support the same methods
|
|
206
|
+
// as Apps Script so we'll fake that too
|
|
207
|
+
return newPeeker(fileit)
|
|
208
|
+
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* this gets an intertor to fetch all the parents meta data
|
|
213
|
+
* @param {FakeDriveMeta} {file} the meta data
|
|
214
|
+
* @returns {object} {Peeker}
|
|
215
|
+
*/
|
|
216
|
+
const getParentsIterator = ({
|
|
217
|
+
file
|
|
218
|
+
}) => {
|
|
219
|
+
|
|
220
|
+
Utils.assertType(file, "object")
|
|
221
|
+
Utils.assertType(file.parents, "array")
|
|
222
|
+
|
|
223
|
+
function* filesink() {
|
|
224
|
+
// the result tank, we just get them all by id
|
|
225
|
+
let tank = file.parents.map(id => getFileById({ id, allow404: false }))
|
|
226
|
+
|
|
227
|
+
while (tank.length) {
|
|
228
|
+
yield newFakeDriveFolder(tank.splice(0, 1)[0])
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// create the iterator
|
|
233
|
+
const parentsIt = filesink()
|
|
234
|
+
|
|
235
|
+
// a regular iterator doesnt support the same methods
|
|
236
|
+
// as Apps Script so we'll fake that too
|
|
237
|
+
return newPeeker(parentsIt)
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* shared get any kind of file meta data by its id
|
|
243
|
+
* @param {string} id the file id
|
|
244
|
+
* @param {function} [handler = handleError]
|
|
245
|
+
* @param {TODO} [fields] the fields to fetch
|
|
246
|
+
* @returns {object} {File, FakeHttpResponse}
|
|
247
|
+
*/
|
|
248
|
+
const fetchFile = ({ id, handler = handleError, fields }) => {
|
|
249
|
+
Utils.assertType(id, "string")
|
|
250
|
+
Utils.assertType(handler, "function")
|
|
251
|
+
|
|
252
|
+
const params = decorateParams({
|
|
253
|
+
fields, min: minFields, params: { fileId: id }
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// TODO need to handle muteHttpExceptions and any other options
|
|
257
|
+
// use the sync version of the drive gapi api
|
|
258
|
+
const { data, response } = Syncit.fxDrive({ prop: "files", method: "get", params })
|
|
259
|
+
return {
|
|
260
|
+
file: data,
|
|
261
|
+
response
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* a File type returned from the json api with my default fields applied
|
|
268
|
+
* there could be others if custom fields are returned
|
|
269
|
+
* @typedef File
|
|
270
|
+
* @property {string} id the id
|
|
271
|
+
* @property {string} name the name
|
|
272
|
+
* @property {string} mimeType the mimetype
|
|
273
|
+
* @property {string[]} parents ids of parents
|
|
274
|
+
*/
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* basic fake File meta data
|
|
278
|
+
* @class FakeDriveMeta
|
|
279
|
+
* @returns {FakeDriveMeta}
|
|
280
|
+
*/
|
|
281
|
+
export class FakeDriveMeta {
|
|
282
|
+
/**
|
|
283
|
+
*
|
|
284
|
+
* @constructor
|
|
285
|
+
* @param {File} meta data from json api
|
|
286
|
+
* @returns {FakeDriveMeta}
|
|
287
|
+
*/
|
|
288
|
+
constructor(meta) {
|
|
289
|
+
this.meta = meta
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* get the file id
|
|
295
|
+
* @returns {string} the file id
|
|
296
|
+
*/
|
|
297
|
+
getId() {
|
|
298
|
+
return this.meta.id
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* get the file name
|
|
303
|
+
* @returns {string} the file name
|
|
304
|
+
*/
|
|
305
|
+
getName() {
|
|
306
|
+
return this.meta.name
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* get the file mimetype
|
|
311
|
+
* @returns {string} the file mimetpe
|
|
312
|
+
*/
|
|
313
|
+
getMimeType() {
|
|
314
|
+
return this.meta.mimeType
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* get the ids of the parents
|
|
319
|
+
* @returns {string[]} the file parents
|
|
320
|
+
*/
|
|
321
|
+
getParents() {
|
|
322
|
+
return getParentsIterator({ file: this.meta })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* for enhancing the file with fields not retrieved by default
|
|
327
|
+
* @param {string|string[]} [field=[]] the required fields
|
|
328
|
+
* @return {FakeDriveMeta} self
|
|
329
|
+
*/
|
|
330
|
+
decorateWithFields(fields = []) {
|
|
331
|
+
const newMeta = getFileById({ id: this.getId(), fields, allow404: false })
|
|
332
|
+
// need to merge this with already known fields
|
|
333
|
+
this.meta = { ...this.meta, ...newMeta }
|
|
334
|
+
return this
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* the meta data for the following fields are not fetched by default
|
|
339
|
+
*/
|
|
340
|
+
getDecorated(prop) {
|
|
341
|
+
return this.decorateWithFields(prop).meta[prop]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
getSize() {
|
|
346
|
+
// the meta is actually a string so convert
|
|
347
|
+
return parseInt(this.getDecorated("size"), 10)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
getDateCreated() {
|
|
351
|
+
return new Date(this.getDecorated("createdTime"))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getDescription() {
|
|
355
|
+
// the meta can be undefined so return null
|
|
356
|
+
const d = this.getDecorated("description")
|
|
357
|
+
return Utils.isUndefined(d) ? null : d
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getLastUpdated() {
|
|
361
|
+
return new Date(this.getDecorated("modifiedTime"))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
isStarred() {
|
|
365
|
+
return this.getDecorated("starred")
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
isTrashed() {
|
|
369
|
+
return this.getDecorated("trashed")
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* basic fake File
|
|
376
|
+
* TODO add lots more methods
|
|
377
|
+
* @class FakeDriveFile
|
|
378
|
+
* @extends FakeDriveMeta
|
|
379
|
+
* @returns {FakeDriveFile}
|
|
380
|
+
*/
|
|
381
|
+
export class FakeDriveFile extends FakeDriveMeta {
|
|
382
|
+
/**
|
|
383
|
+
*
|
|
384
|
+
* @constructor
|
|
385
|
+
* @param {File} meta data from json api
|
|
386
|
+
* @returns {FakeDriveFile}
|
|
387
|
+
*/
|
|
388
|
+
constructor(meta) {
|
|
389
|
+
super(meta)
|
|
390
|
+
if (isFolder(meta)) {
|
|
391
|
+
throw new Error(`file cant be a folder:` + JSON.stringify(meta))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
*
|
|
397
|
+
* @returns {FakeBlob}
|
|
398
|
+
*/
|
|
399
|
+
getBlob() {
|
|
400
|
+
// spawn child process to syncify getting content as by array
|
|
401
|
+
const { data } = Syncit.fxDriveMedia({ id: this.getId() })
|
|
402
|
+
// and blobify
|
|
403
|
+
return Utilities.newBlob(data, this.getName(), this.getMimeType())
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* basic fake Folder
|
|
410
|
+
* TODO add lots more methods
|
|
411
|
+
* @class FakeDriveFolder
|
|
412
|
+
* @extends FakeDriveFile
|
|
413
|
+
* @returns {FakeDriveFolder}
|
|
414
|
+
*/
|
|
415
|
+
export class FakeDriveFolder extends FakeDriveMeta {
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
*
|
|
419
|
+
* @constructor
|
|
420
|
+
* @param {File} meta data from json api
|
|
421
|
+
* @returns {FakeDriveFile}
|
|
422
|
+
*/
|
|
423
|
+
constructor(meta) {
|
|
424
|
+
super(meta)
|
|
425
|
+
if (!isFolder(meta)) {
|
|
426
|
+
throw new Error(`file must be a folder:` + JSON.stringify(meta))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
//TODO something wrong with this
|
|
431
|
+
get _isRoot() {
|
|
432
|
+
return Boolean(this.getParents() || !this.getParents().length)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* get files in this folder
|
|
437
|
+
* @return {FakeDriveFileIterator}
|
|
438
|
+
*/
|
|
439
|
+
getFiles() {
|
|
440
|
+
return getFilesIterator({
|
|
441
|
+
parentId: this.getId(),
|
|
442
|
+
folderTypes: false,
|
|
443
|
+
fileTypes: true
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* get folders in this folder
|
|
449
|
+
* @return {FakeDriveFileIterator}
|
|
450
|
+
*/
|
|
451
|
+
getFolders() {
|
|
452
|
+
return getFilesIterator({
|
|
453
|
+
parentId: this.getId(),
|
|
454
|
+
fileTypes: false,
|
|
455
|
+
folderTypes: true
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* get file by Id
|
|
461
|
+
* folders can get files
|
|
462
|
+
* @param {string} id
|
|
463
|
+
* @returns {FakeDriveFile|null}
|
|
464
|
+
*/
|
|
465
|
+
getFileById(id) {
|
|
466
|
+
const file = getFileById({ id })
|
|
467
|
+
return file ? newFakeDriveFile(file) : null
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* get folder by Id
|
|
472
|
+
* folders can get files
|
|
473
|
+
* @param {string} id
|
|
474
|
+
* @returns {FakeDriveFolder|null}
|
|
475
|
+
*/
|
|
476
|
+
getFolderById(id) {
|
|
477
|
+
const file = getFileById({ id })
|
|
478
|
+
return file ? newFakeDriveFolder(file) : null
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* get folders by name
|
|
483
|
+
* @param {string} name
|
|
484
|
+
* @return {FakeDriveFileIterator}
|
|
485
|
+
*/
|
|
486
|
+
getFoldersByName(name) {
|
|
487
|
+
return getFilesIterator({
|
|
488
|
+
parentId: this.getId(),
|
|
489
|
+
fileTypes: false,
|
|
490
|
+
folderTypes: true,
|
|
491
|
+
qob: [`name='${name}'`]
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* get files by name
|
|
496
|
+
* @param {string} name
|
|
497
|
+
* @return {FakeDriveFileIterator}
|
|
498
|
+
*/
|
|
499
|
+
getFilesByName(name) {
|
|
500
|
+
return getFilesIterator({
|
|
501
|
+
parentId: this.getId(),
|
|
502
|
+
fileTypes: true,
|
|
503
|
+
folderTypes: false,
|
|
504
|
+
qob: [`name='${name}'`]
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* basic fake DriveApp
|
|
512
|
+
* TODO add lots more methods
|
|
513
|
+
* @class FakeDriveApp
|
|
514
|
+
* @extends FakeDriveFolder
|
|
515
|
+
* @returns {FakeDriveApp}
|
|
516
|
+
*/
|
|
517
|
+
export class FakeDriveApp {
|
|
518
|
+
|
|
519
|
+
constructor() {
|
|
520
|
+
const file = getFileById({ id: 'root', allow404: false })
|
|
521
|
+
this.rootFolder = newFakeDriveFolder(file)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* get folder by Id
|
|
526
|
+
* folders can get files
|
|
527
|
+
* @returns {FakeDriveFolder}
|
|
528
|
+
*/
|
|
529
|
+
getRootFolder() {
|
|
530
|
+
const file = getFileById({ id: 'root', allow404: false })
|
|
531
|
+
return newFakeDriveFolder(file)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
getFileById(id) {
|
|
535
|
+
return this.rootFolder.getFileById(id)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
getFolderById(id) {
|
|
539
|
+
return this.rootFolder.getFolderById(id)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* get folders by name
|
|
544
|
+
* @param {string} name
|
|
545
|
+
* @return {FakeDriveFileIterator}
|
|
546
|
+
*/
|
|
547
|
+
getFoldersByName(name) {
|
|
548
|
+
return getFilesIterator({
|
|
549
|
+
fileTypes: false,
|
|
550
|
+
folderTypes: true,
|
|
551
|
+
qob: [`name='${name}'`]
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* get folders by name
|
|
557
|
+
* @param {string} name
|
|
558
|
+
* @return {FakeDriveFileIterator}
|
|
559
|
+
*/
|
|
560
|
+
getFilesByName(name) {
|
|
561
|
+
return getFilesIterator({
|
|
562
|
+
fileTypes: true,
|
|
563
|
+
folderTypes: false,
|
|
564
|
+
qob: [`name='${name}'`]
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Utils } from '../../support/utils.js'
|
|
2
|
+
import { folderType } from '../../support/constants.js'
|
|
3
|
+
/**
|
|
4
|
+
* utilities for drive access shared between all fakedrive classes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* default error handler
|
|
9
|
+
* @param {FakeHttpResponse} response
|
|
10
|
+
* @returns {Boolean}
|
|
11
|
+
*/
|
|
12
|
+
export const handleError = (response) => {
|
|
13
|
+
if (is404(response)) {
|
|
14
|
+
return null
|
|
15
|
+
} else {
|
|
16
|
+
throwResponse(response)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* check if a drive reponse good
|
|
22
|
+
* @param {SyncDriveResponse} response
|
|
23
|
+
* @returns {Boolean}
|
|
24
|
+
*/
|
|
25
|
+
export const isGood = (response) => Math.floor(response.status / 100) === 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* check if a SyncDriveResponse is a not found
|
|
30
|
+
* @param {SyncDriveResponse} response
|
|
31
|
+
* @returns {Boolean}
|
|
32
|
+
*/
|
|
33
|
+
const is404 = (response) => response.status === 404
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* file mimetype is a folder
|
|
39
|
+
* @param {File} file
|
|
40
|
+
* @returns {Boolean}
|
|
41
|
+
*/
|
|
42
|
+
export const isFolder = (file) => file.mimeType === folderType
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* general throw when a reponse is bad
|
|
46
|
+
* @param {SyncDriveResponse} response the response from a fake fetch
|
|
47
|
+
*/
|
|
48
|
+
export const throwResponse = (response) => {
|
|
49
|
+
throw new Error(`status: ${response.status} : ${response.statusText}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fileFields = ["name", "parents", "id", "mimeType"]
|
|
53
|
+
/**
|
|
54
|
+
* minimum fields i'll retrieve
|
|
55
|
+
* @constant
|
|
56
|
+
* @type {object}
|
|
57
|
+
* @default
|
|
58
|
+
*/
|
|
59
|
+
export const minFields = fileFields
|
|
60
|
+
export const minFieldsList = ["nextPageToken", { files: minFields }]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* in preparation for merginhg field specifications
|
|
64
|
+
* @param {string|object []} spec the fields specs to prepare
|
|
65
|
+
* @param {string|object []} [model] what it should look like
|
|
66
|
+
* @return {Map} this normalized map can be used to remove duplicates when merging
|
|
67
|
+
*/
|
|
68
|
+
const reduceFields = (spec, model) => {
|
|
69
|
+
|
|
70
|
+
const reduced = Utils.arrify(spec).reduce((p, c) => {
|
|
71
|
+
if (Utils.isString(c)) {
|
|
72
|
+
// so this would generate at top level
|
|
73
|
+
p.set(c, new Set())
|
|
74
|
+
} else {
|
|
75
|
+
if (!Utils.isObject(c)) {
|
|
76
|
+
throw new Error(`field format should be like ${JSON.stringify(model)}`)
|
|
77
|
+
}
|
|
78
|
+
//we end up with someting like Map{x:Set(null), file: Set(name,id,etc) which will allow merging of multiple of these
|
|
79
|
+
Reflect.ownKeys(c).forEach(k => {
|
|
80
|
+
if (!p.has(k)) p.set(k, new Set())
|
|
81
|
+
Utils.arrify(c[k] || []).forEach(f => p.get(k).add(f))
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
return p
|
|
85
|
+
}, new Map())
|
|
86
|
+
|
|
87
|
+
return reduced
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* make url params for fields
|
|
92
|
+
* @param {object} [extras={}] any extra fields
|
|
93
|
+
* @param {object} [min=minFields] the minum fields we need
|
|
94
|
+
* @return {string} translated to fields=... as url params
|
|
95
|
+
*/
|
|
96
|
+
export const fieldUrlParams = (extras = {}, min = minFields) => {
|
|
97
|
+
const extrasOb = reduceFields(extras, min)
|
|
98
|
+
const minOb = reduceFields(min, min)
|
|
99
|
+
|
|
100
|
+
for (const [k, s] of extrasOb) {
|
|
101
|
+
if (!minOb.has(k)) {
|
|
102
|
+
minOb.set(k, new Set())
|
|
103
|
+
}
|
|
104
|
+
const mink = minOb.has(k) ? Array.from(minOb.get(k).keys()) : []
|
|
105
|
+
const mrg = Array.from(s.keys()).concat(mink)
|
|
106
|
+
minOb.set(k, new Set(mrg))
|
|
107
|
+
}
|
|
108
|
+
const fields = []
|
|
109
|
+
for (const [k, s] of minOb) {
|
|
110
|
+
if (s.size) {
|
|
111
|
+
fields.push(`${k}(${Array.from(s.keys()).join(",")})`)
|
|
112
|
+
} else {
|
|
113
|
+
fields.push(k)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// now make into a string
|
|
118
|
+
return fields.join(",")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
*
|
|
123
|
+
* @param {object} p the params
|
|
124
|
+
* @param {string} p.url the bare url
|
|
125
|
+
* @param {object|object []} p.min the min fields required
|
|
126
|
+
* @param {object|undefined|object[]} [p.fields] the fields to add to the minimum required
|
|
127
|
+
* @param {object} p.params and other kinds of params
|
|
128
|
+
* @returns {string} the decorated url
|
|
129
|
+
*/
|
|
130
|
+
export const decorateUrl = ({ url, fields, params, min }) => {
|
|
131
|
+
// add the basic fields parameters
|
|
132
|
+
// annoyance:
|
|
133
|
+
// note that for list we need stuff like file(id,name) etc..
|
|
134
|
+
// but for get by id, we just want id,name
|
|
135
|
+
const d = decorateParams({fields, params, min})
|
|
136
|
+
return `${url}?${d}`
|
|
137
|
+
}
|
|
138
|
+
export const decorateParams = ({ fields, params = {}, min = minFields }) => {
|
|
139
|
+
const fp = fieldUrlParams(fields, min)
|
|
140
|
+
return Utils.makeParamOb({ fields: fp, ...params })
|
|
141
|
+
}
|