google-spreadsheet 3.3.0 → 4.0.1

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.
@@ -0,0 +1,636 @@
1
+ import Axios, {
2
+ AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig,
3
+ } from 'axios';
4
+
5
+ import { Stream } from 'stream';
6
+ import * as _ from './lodash';
7
+ import { GoogleSpreadsheetWorksheet } from './GoogleSpreadsheetWorksheet';
8
+ import { axiosParamsSerializer, getFieldMask } from './utils';
9
+ import {
10
+ DataFilter, GridRange, NamedRangeId, SpreadsheetId, SpreadsheetProperties, WorksheetId, WorksheetProperties,
11
+ } from './types/sheets-types';
12
+ import { PermissionRoles, PermissionsList, PublicPermissionRoles } from './types/drive-types';
13
+ import { RecursivePartial } from './types/util-types';
14
+ import { AUTH_MODES, GoogleApiAuth } from './types/auth-types';
15
+
16
+
17
+ const SHEETS_API_BASE_URL = 'https://sheets.googleapis.com/v4/spreadsheets';
18
+ const DRIVE_API_BASE_URL = 'https://www.googleapis.com/drive/v3/files';
19
+
20
+ const EXPORT_CONFIG: Record<string, { singleWorksheet?: boolean }> = {
21
+ html: {},
22
+ zip: {},
23
+ xlsx: {},
24
+ ods: {},
25
+ csv: { singleWorksheet: true },
26
+ tsv: { singleWorksheet: true },
27
+ pdf: { singleWorksheet: true },
28
+ };
29
+ type ExportFileTypes = keyof typeof EXPORT_CONFIG;
30
+
31
+
32
+
33
+
34
+ function getAuthMode(auth: GoogleApiAuth) {
35
+ if ('getRequestHeaders' in auth) return AUTH_MODES.GOOGLE_AUTH_CLIENT;
36
+ if ('token' in auth) return AUTH_MODES.RAW_ACCESS_TOKEN;
37
+ if ('apiKey' in auth) return AUTH_MODES.API_KEY;
38
+ throw new Error('Invalid auth');
39
+ }
40
+
41
+ async function getRequestAuthConfig(auth: GoogleApiAuth) {
42
+ // google-auth-libary methods all can call this method to get the right headers
43
+ // JWT | OAuth2Client | GoogleAuth | Impersonate | AuthClient
44
+ if ('getRequestHeaders' in auth) {
45
+ const headers = await auth.getRequestHeaders();
46
+ return { headers };
47
+ }
48
+
49
+ // API key only access passes through the api key as a query param
50
+ // (note this can only provide read-only access)
51
+ if ('apiKey' in auth) {
52
+ return { params: { key: auth.apiKey } };
53
+ }
54
+
55
+ // RAW ACCESS TOKEN
56
+ if ('token' in auth) {
57
+ return { headers: { Authorization: `Bearer ${auth.token}` } };
58
+ }
59
+
60
+ throw new Error('Invalid auth');
61
+ }
62
+
63
+ /**
64
+ * Google Sheets document
65
+ *
66
+ * @description
67
+ * **This class represents an entire google spreadsheet document**
68
+ * Provides methods to interact with document metadata/settings, formatting, manage sheets, and acts as the main gateway to interacting with sheets and data that the document contains.q
69
+ *
70
+ */
71
+ export class GoogleSpreadsheet {
72
+ readonly spreadsheetId: string;
73
+
74
+ public auth: GoogleApiAuth;
75
+ get authMode() {
76
+ return getAuthMode(this.auth);
77
+ }
78
+
79
+ private _rawSheets: any;
80
+ private _rawProperties = null as SpreadsheetProperties | null;
81
+ private _spreadsheetUrl = null as string | null;
82
+ private _deleted = false;
83
+
84
+ /**
85
+ * Sheets API [axios](https://axios-http.com) instance
86
+ * authentication is automatically attached
87
+ * can be used if unsupported sheets calls need to be made
88
+ * @see https://developers.google.com/sheets/api/reference/rest
89
+ * */
90
+ readonly sheetsApi: AxiosInstance;
91
+
92
+ /**
93
+ * Drive API [axios](https://axios-http.com) instance
94
+ * authentication automatically attached
95
+ * can be used if unsupported drive calls need to be made
96
+ * @topic permissions
97
+ * @see https://developers.google.com/drive/api/v3/reference
98
+ * */
99
+ readonly driveApi: AxiosInstance;
100
+
101
+
102
+ /**
103
+ * initialize new GoogleSpreadsheet
104
+ * @category Initialization
105
+ * */
106
+ constructor(
107
+ /** id of google spreadsheet doc */
108
+ spreadsheetId: SpreadsheetId,
109
+ /** authentication to use with Google Sheets API */
110
+ auth: GoogleApiAuth
111
+ ) {
112
+ this.spreadsheetId = spreadsheetId;
113
+ this.auth = auth;
114
+
115
+ this._rawSheets = {};
116
+ this._spreadsheetUrl = null;
117
+
118
+ // create an axios instance with sheet root URL and interceptors to handle auth
119
+ this.sheetsApi = Axios.create({
120
+ baseURL: `${SHEETS_API_BASE_URL}/${spreadsheetId}`,
121
+ paramsSerializer: axiosParamsSerializer,
122
+ // removing limits in axios for large requests
123
+ // https://stackoverflow.com/questions/56868023/error-request-body-larger-than-maxbodylength-limit-when-sending-base64-post-req
124
+ maxContentLength: Infinity,
125
+ maxBodyLength: Infinity,
126
+ });
127
+ this.driveApi = Axios.create({
128
+ baseURL: `${DRIVE_API_BASE_URL}/${spreadsheetId}`,
129
+ paramsSerializer: axiosParamsSerializer,
130
+ });
131
+ // have to use bind here or the functions dont have access to `this` :(
132
+ this.sheetsApi.interceptors.request.use(this._setAxiosRequestAuth.bind(this));
133
+ this.sheetsApi.interceptors.response.use(
134
+ this._handleAxiosResponse.bind(this),
135
+ this._handleAxiosErrors.bind(this)
136
+ );
137
+ this.driveApi.interceptors.request.use(this._setAxiosRequestAuth.bind(this));
138
+ this.driveApi.interceptors.response.use(
139
+ this._handleAxiosResponse.bind(this),
140
+ this._handleAxiosErrors.bind(this)
141
+ );
142
+ }
143
+
144
+
145
+ // AUTH RELATED FUNCTIONS ////////////////////////////////////////////////////////////////////////
146
+
147
+ // INTERNAL UTILITY FUNCTIONS ////////////////////////////////////////////////////////////////////
148
+
149
+ /** @internal */
150
+ async _setAxiosRequestAuth(config: InternalAxiosRequestConfig) {
151
+ const authConfig = await getRequestAuthConfig(this.auth);
152
+ _.each(authConfig.headers, (val, key) => {
153
+ config.headers.set(key, val);
154
+ });
155
+ config.params = { ...config.params, ...authConfig.params };
156
+ return config;
157
+ }
158
+
159
+ /** @internal */
160
+ async _handleAxiosResponse(response: AxiosResponse) { return response; }
161
+ /** @internal */
162
+ async _handleAxiosErrors(error: AxiosError) {
163
+ // console.log(error);
164
+ const errorData = error.response?.data as any;
165
+
166
+ if (errorData) {
167
+ // usually the error has a code and message, but occasionally not
168
+ if (!errorData.error) throw error;
169
+
170
+ const { code, message } = errorData.error;
171
+ error.message = `Google API error - [${code}] ${message}`;
172
+ throw error;
173
+ }
174
+
175
+ if (_.get(error, 'response.status') === 403) {
176
+ if ('apiKey' in this.auth) {
177
+ throw new Error('Sheet is private. Use authentication or make public. (see https://github.com/theoephraim/node-google-spreadsheet#a-note-on-authentication for details)');
178
+ }
179
+ }
180
+ throw error;
181
+ }
182
+
183
+ /** @internal */
184
+ async _makeSingleUpdateRequest(requestType: string, requestParams: any) {
185
+ const response = await this.sheetsApi.post(':batchUpdate', {
186
+ requests: [{ [requestType]: requestParams }],
187
+ includeSpreadsheetInResponse: true,
188
+ // responseRanges: [string]
189
+ // responseIncludeGridData: true
190
+ });
191
+
192
+ this._updateRawProperties(response.data.updatedSpreadsheet.properties);
193
+ _.each(response.data.updatedSpreadsheet.sheets, (s) => this._updateOrCreateSheet(s));
194
+ // console.log('API RESPONSE', response.data.replies[0][requestType]);
195
+ return response.data.replies[0][requestType];
196
+ }
197
+
198
+ // TODO: review these types
199
+ // currently only used in batching cell updates
200
+ /** @internal */
201
+ async _makeBatchUpdateRequest(requests: any[], responseRanges?: string | string[]) {
202
+ // this is used for updating batches of cells
203
+ const response = await this.sheetsApi.post(':batchUpdate', {
204
+ requests,
205
+ includeSpreadsheetInResponse: true,
206
+ ...responseRanges && {
207
+ responseIncludeGridData: true,
208
+ ...responseRanges !== '*' && { responseRanges },
209
+ },
210
+ });
211
+
212
+ this._updateRawProperties(response.data.updatedSpreadsheet.properties);
213
+ _.each(response.data.updatedSpreadsheet.sheets, (s) => this._updateOrCreateSheet(s));
214
+ }
215
+
216
+ /** @internal */
217
+ _ensureInfoLoaded() {
218
+ if (!this._rawProperties) throw new Error('You must call `doc.loadInfo()` before accessing this property');
219
+ }
220
+
221
+ /** @internal */
222
+ _updateRawProperties(newProperties: SpreadsheetProperties) { this._rawProperties = newProperties; }
223
+
224
+ /** @internal */
225
+ _updateOrCreateSheet(sheetInfo: { properties: WorksheetProperties, data: any }) {
226
+ const { properties, data } = sheetInfo;
227
+ const { sheetId } = properties;
228
+ if (!this._rawSheets[sheetId]) {
229
+ this._rawSheets[sheetId] = new GoogleSpreadsheetWorksheet(this, properties, data);
230
+ } else {
231
+ this._rawSheets[sheetId].updateRawData(properties, data);
232
+ }
233
+ }
234
+
235
+ // BASIC PROPS //////////////////////////////////////////////////////////////////////////////
236
+ _getProp(param: keyof SpreadsheetProperties) {
237
+ this._ensureInfoLoaded();
238
+ // ideally ensureInfoLoaded would assert that _rawProperties is in fact loaded
239
+ // but this is not currently possible in TS - see https://github.com/microsoft/TypeScript/issues/49709
240
+ return this._rawProperties![param];
241
+ }
242
+
243
+ get title(): SpreadsheetProperties['title'] { return this._getProp('title'); }
244
+ get locale(): SpreadsheetProperties['locale'] { return this._getProp('locale'); }
245
+ get timeZone(): SpreadsheetProperties['timeZone'] { return this._getProp('timeZone'); }
246
+ get autoRecalc(): SpreadsheetProperties['autoRecalc'] { return this._getProp('autoRecalc'); }
247
+ get defaultFormat(): SpreadsheetProperties['defaultFormat'] { return this._getProp('defaultFormat'); }
248
+ get spreadsheetTheme(): SpreadsheetProperties['spreadsheetTheme'] { return this._getProp('spreadsheetTheme'); }
249
+ get iterativeCalculationSettings(): SpreadsheetProperties['iterativeCalculationSettings'] { return this._getProp('iterativeCalculationSettings'); }
250
+
251
+ /**
252
+ * update spreadsheet properties
253
+ * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#SpreadsheetProperties
254
+ * */
255
+ async updateProperties(properties: Partial<SpreadsheetProperties>) {
256
+ await this._makeSingleUpdateRequest('updateSpreadsheetProperties', {
257
+ properties,
258
+ fields: getFieldMask(properties),
259
+ });
260
+ }
261
+
262
+ // BASIC INFO ////////////////////////////////////////////////////////////////////////////////////
263
+ async loadInfo(includeCells = false) {
264
+ const response = await this.sheetsApi.get('/', {
265
+ params: {
266
+ ...includeCells && { includeGridData: true },
267
+ },
268
+ });
269
+ this._spreadsheetUrl = response.data.spreadsheetUrl;
270
+ this._rawProperties = response.data.properties;
271
+ _.each(response.data.sheets, (s) => this._updateOrCreateSheet(s));
272
+ }
273
+
274
+ resetLocalCache() {
275
+ this._rawProperties = null;
276
+ this._rawSheets = {};
277
+ }
278
+
279
+ // WORKSHEETS ////////////////////////////////////////////////////////////////////////////////////
280
+ get sheetCount() {
281
+ this._ensureInfoLoaded();
282
+ return _.values(this._rawSheets).length;
283
+ }
284
+
285
+ get sheetsById(): Record<WorksheetId, GoogleSpreadsheetWorksheet> {
286
+ this._ensureInfoLoaded();
287
+ return this._rawSheets;
288
+ }
289
+
290
+ get sheetsByIndex(): GoogleSpreadsheetWorksheet[] {
291
+ this._ensureInfoLoaded();
292
+ return _.sortBy(this._rawSheets, 'index');
293
+ }
294
+
295
+ get sheetsByTitle(): Record<string, GoogleSpreadsheetWorksheet> {
296
+ this._ensureInfoLoaded();
297
+ return _.keyBy(this._rawSheets, 'title');
298
+ }
299
+
300
+ /**
301
+ * Add new worksheet to document
302
+ * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddSheetRequest
303
+ * */
304
+ async addSheet(
305
+ properties: Partial<
306
+ RecursivePartial<WorksheetProperties>
307
+ & {
308
+ headerValues: string[],
309
+ headerRowIndex: number
310
+ }
311
+ > = {}
312
+ ) {
313
+ const response = await this._makeSingleUpdateRequest('addSheet', {
314
+ properties: _.omit(properties, 'headerValues', 'headerRowIndex'),
315
+ });
316
+ // _makeSingleUpdateRequest already adds the sheet
317
+ const newSheetId = response.properties.sheetId;
318
+ const newSheet = this.sheetsById[newSheetId];
319
+
320
+ if (properties.headerValues) {
321
+ await newSheet.setHeaderRow(properties.headerValues, properties.headerRowIndex);
322
+ }
323
+
324
+ return newSheet;
325
+ }
326
+
327
+ /**
328
+ * delete a worksheet
329
+ * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteSheetRequest
330
+ * */
331
+ async deleteSheet(sheetId: WorksheetId) {
332
+ await this._makeSingleUpdateRequest('deleteSheet', { sheetId });
333
+ delete this._rawSheets[sheetId];
334
+ }
335
+
336
+ // NAMED RANGES //////////////////////////////////////////////////////////////////////////////////
337
+
338
+ /**
339
+ * create a new named range
340
+ * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddNamedRangeRequest
341
+ */
342
+ async addNamedRange(
343
+ /** name of new named range */
344
+ name: string,
345
+ /** GridRange object describing range */
346
+ range: GridRange,
347
+ /** id for named range (optional) */
348
+ namedRangeId?: string
349
+ ) {
350
+ // TODO: add named range to local cache
351
+ return this._makeSingleUpdateRequest('addNamedRange', {
352
+ name,
353
+ namedRangeId,
354
+ range,
355
+ });
356
+ }
357
+
358
+ /**
359
+ * delete a named range
360
+ * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteNamedRangeRequest
361
+ * */
362
+ async deleteNamedRange(
363
+ /** id of named range to delete */
364
+ namedRangeId: NamedRangeId
365
+ ) {
366
+ // TODO: remove named range from local cache
367
+ return this._makeSingleUpdateRequest('deleteNamedRange', { namedRangeId });
368
+ }
369
+
370
+ // LOADING CELLS /////////////////////////////////////////////////////////////////////////////////
371
+
372
+ /** fetch cell data into local cache */
373
+ async loadCells(
374
+ /**
375
+ * single filter or array of filters
376
+ * strings are treated as A1 ranges, objects are treated as GridRange objects
377
+ * pass nothing to fetch all cells
378
+ * */
379
+ filters?: DataFilter | DataFilter[]
380
+ ) {
381
+ // TODO: make it support DeveloperMetadataLookup objects
382
+
383
+
384
+
385
+ // TODO: switch to this mode if using a read-only auth token?
386
+ const readOnlyMode = this.authMode === AUTH_MODES.API_KEY;
387
+
388
+ const filtersArray = _.isArray(filters) ? filters : [filters];
389
+ const dataFilters = _.map(filtersArray, (filter) => {
390
+ if (_.isString(filter)) {
391
+ return readOnlyMode ? filter : { a1Range: filter };
392
+ }
393
+ if (_.isObject(filter)) {
394
+ if (readOnlyMode) {
395
+ throw new Error('Only A1 ranges are supported when fetching cells with read-only access (using only an API key)');
396
+ }
397
+ // TODO: make this support Developer Metadata filters
398
+ return { gridRange: filter };
399
+ }
400
+ throw new Error('Each filter must be an A1 range string or a gridrange object');
401
+ });
402
+
403
+ let result;
404
+ // when using an API key only, we must use the regular get endpoint
405
+ // because :getByDataFilter requires higher access
406
+ if (this.authMode === AUTH_MODES.API_KEY) {
407
+ result = await this.sheetsApi.get('/', {
408
+ params: {
409
+ includeGridData: true,
410
+ ranges: dataFilters,
411
+ },
412
+ });
413
+ // otherwise we use the getByDataFilter endpoint because it is more flexible
414
+ } else {
415
+ result = await this.sheetsApi.post(':getByDataFilter', {
416
+ includeGridData: true,
417
+ dataFilters,
418
+ });
419
+ }
420
+
421
+ const { sheets } = result.data;
422
+ _.each(sheets, (sheet) => { this._updateOrCreateSheet(sheet); });
423
+ }
424
+
425
+ // EXPORTING /////////////////////////////////////////////////////////////
426
+
427
+ /**
428
+ * export/download helper, not meant to be called directly (use downloadAsX methods on spreadsheet and worksheet instead)
429
+ * @internal
430
+ */
431
+ async _downloadAs(
432
+ fileType: ExportFileTypes,
433
+ worksheetId: WorksheetId | undefined,
434
+ returnStreamInsteadOfBuffer?: boolean
435
+ ) {
436
+ // see https://stackoverflow.com/questions/11619805/using-the-google-drive-api-to-download-a-spreadsheet-in-csv-format/51235960#51235960
437
+
438
+ if (!EXPORT_CONFIG[fileType]) throw new Error(`unsupported export fileType - ${fileType}`);
439
+ if (EXPORT_CONFIG[fileType].singleWorksheet) {
440
+ if (worksheetId === undefined) throw new Error(`Must specify worksheetId when exporting as ${fileType}`);
441
+ } else if (worksheetId) throw new Error(`Cannot specify worksheetId when exporting as ${fileType}`);
442
+
443
+ // google UI shows "html" but passes through "zip"
444
+ if (fileType === 'html') fileType = 'zip';
445
+
446
+ if (!this._spreadsheetUrl) throw new Error('Cannot export sheet that is not fully loaded');
447
+
448
+ const exportUrl = this._spreadsheetUrl.replace('/edit', '/export');
449
+ const response = await this.sheetsApi.get(exportUrl, {
450
+ baseURL: '', // unset baseUrl since we're not hitting the normal sheets API
451
+ params: {
452
+ id: this.spreadsheetId,
453
+ format: fileType,
454
+ ...worksheetId && { gid: worksheetId },
455
+ },
456
+ responseType: returnStreamInsteadOfBuffer ? 'stream' : 'arraybuffer',
457
+ });
458
+ return response.data;
459
+ }
460
+
461
+ /**
462
+ * exports entire document as html file (zipped)
463
+ * @topic export
464
+ * */
465
+ async downloadAsZippedHTML(): Promise<ArrayBuffer>;
466
+ async downloadAsZippedHTML(returnStreamInsteadOfBuffer: false): Promise<ArrayBuffer>;
467
+ async downloadAsZippedHTML(returnStreamInsteadOfBuffer: true): Promise<Stream>;
468
+ async downloadAsZippedHTML(returnStreamInsteadOfBuffer?: boolean) {
469
+ return this._downloadAs('html', undefined, returnStreamInsteadOfBuffer);
470
+ }
471
+
472
+ /**
473
+ * @deprecated
474
+ * use `doc.downloadAsZippedHTML()` instead
475
+ * */
476
+ async downloadAsHTML(returnStreamInsteadOfBuffer?: boolean) {
477
+ return this._downloadAs('html', undefined, returnStreamInsteadOfBuffer);
478
+ }
479
+
480
+ /**
481
+ * exports entire document as xlsx spreadsheet (Microsoft Office Excel)
482
+ * @topic export
483
+ * */
484
+ async downloadAsXLSX(): Promise<ArrayBuffer>;
485
+ async downloadAsXLSX(returnStreamInsteadOfBuffer: false): Promise<ArrayBuffer>;
486
+ async downloadAsXLSX(returnStreamInsteadOfBuffer: true): Promise<Stream>;
487
+ async downloadAsXLSX(returnStreamInsteadOfBuffer = false) {
488
+ return this._downloadAs('xlsx', undefined, returnStreamInsteadOfBuffer);
489
+ }
490
+ /**
491
+ * exports entire document as ods spreadsheet (Open Office)
492
+ * @topic export
493
+ */
494
+ async downloadAsODS(): Promise<ArrayBuffer>;
495
+ async downloadAsODS(returnStreamInsteadOfBuffer: false): Promise<ArrayBuffer>;
496
+ async downloadAsODS(returnStreamInsteadOfBuffer: true): Promise<Stream>;
497
+ async downloadAsODS(returnStreamInsteadOfBuffer = false) {
498
+ return this._downloadAs('ods', undefined, returnStreamInsteadOfBuffer);
499
+ }
500
+
501
+
502
+ async delete() {
503
+ const response = await this.driveApi.delete('');
504
+ this._deleted = true;
505
+ return response.data;
506
+ }
507
+
508
+ // PERMISSIONS ///////////////////////////////////////////////////////////////////////////////////
509
+
510
+ /**
511
+ * list all permissions entries for doc
512
+ */
513
+ async listPermissions(): Promise<PermissionsList> {
514
+ const listReq = await this.driveApi.request({
515
+ method: 'GET',
516
+ url: '/permissions',
517
+ params: {
518
+ fields: 'permissions(id,type,emailAddress,domain,role,displayName,photoLink,deleted)',
519
+ },
520
+ });
521
+ return listReq.data.permissions as PermissionsList;
522
+ }
523
+
524
+ async setPublicAccessLevel(role: PublicPermissionRoles | false) {
525
+ const permissions = await this.listPermissions();
526
+ const existingPublicPermission = _.find(permissions, (p) => p.type === 'anyone');
527
+
528
+ if (role === false) {
529
+ if (!existingPublicPermission) {
530
+ // doc is already not public... could throw an error or just do nothing
531
+ return;
532
+ }
533
+ await this.driveApi.request({
534
+ method: 'DELETE',
535
+ url: `/permissions/${existingPublicPermission.id}`,
536
+ });
537
+ } else {
538
+ const _shareReq = await this.driveApi.request({
539
+ method: 'POST',
540
+ url: '/permissions',
541
+ params: {
542
+ },
543
+ data: {
544
+ role: role || 'viewer',
545
+ type: 'anyone',
546
+ },
547
+ });
548
+ }
549
+ }
550
+
551
+ /** share document to email or domain */
552
+ async share(emailAddressOrDomain: string, opts?: {
553
+ /** set role level, defaults to owner */
554
+ role?: PermissionRoles,
555
+
556
+ /** set to true if email is for a group */
557
+ isGroup?: boolean,
558
+
559
+ /** set to string to include a custom message, set to false to skip sending a notification altogether */
560
+ emailMessage?: string | false,
561
+
562
+ // moveToNewOwnersRoot?: string,
563
+ // /** send a notification email (default = true) */
564
+ // sendNotificationEmail?: boolean,
565
+ // /** support My Drives and shared drives (default = false) */
566
+ // supportsAllDrives?: boolean,
567
+
568
+ // /** Issue the request as a domain administrator */
569
+ // useDomainAdminAccess?: boolean,
570
+ }) {
571
+ let emailAddress: string | undefined;
572
+ let domain: string | undefined;
573
+ if (emailAddressOrDomain.includes('@')) {
574
+ emailAddress = emailAddressOrDomain;
575
+ } else {
576
+ domain = emailAddressOrDomain;
577
+ }
578
+
579
+
580
+ const shareReq = await this.driveApi.request({
581
+ method: 'POST',
582
+ url: '/permissions',
583
+ params: {
584
+ ...opts?.emailMessage === false && { sendNotificationEmail: false },
585
+ ..._.isString(opts?.emailMessage) && { emailMessage: opts?.emailMessage },
586
+ ...opts?.role === 'owner' && { transferOwnership: true },
587
+ },
588
+ data: {
589
+ role: opts?.role || 'writer',
590
+ ...emailAddress && {
591
+ type: opts?.isGroup ? 'group' : 'user',
592
+ emailAddress,
593
+ },
594
+ ...domain && {
595
+ type: 'domain',
596
+ domain,
597
+ },
598
+ },
599
+ });
600
+
601
+ return shareReq.data;
602
+ }
603
+
604
+ //
605
+ // CREATE NEW DOC ////////////////////////////////////////////////////////////////////////////////
606
+ static async createNewSpreadsheetDocument(auth: GoogleApiAuth, properties?: Partial<SpreadsheetProperties>) {
607
+ // see updateProperties for more info about available properties
608
+
609
+ if ('apiKey' in auth) {
610
+ throw new Error('Cannot use api key only to create a new spreadsheet - it is only usable for read-only access of public docs');
611
+ }
612
+
613
+ // TODO: handle injecting default credentials if running on google infra
614
+
615
+ const authConfig = await getRequestAuthConfig(auth);
616
+
617
+ const response = await Axios.request({
618
+ method: 'POST',
619
+ url: SHEETS_API_BASE_URL,
620
+ paramsSerializer: axiosParamsSerializer,
621
+ ...authConfig, // has the auth header
622
+ data: {
623
+ properties,
624
+ },
625
+ });
626
+
627
+ const newSpreadsheet = new GoogleSpreadsheet(response.data.spreadsheetId, auth);
628
+
629
+ // TODO ideally these things aren't public, might want to refactor anyway
630
+ newSpreadsheet._spreadsheetUrl = response.data.spreadsheetUrl;
631
+ newSpreadsheet._rawProperties = response.data.properties;
632
+ _.each(response.data.sheets, (s) => newSpreadsheet._updateOrCreateSheet(s));
633
+
634
+ return newSpreadsheet;
635
+ }
636
+ }