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.
- package/README.md +73 -34
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +987 -0
- package/dist/index.mjs +1 -0
- package/package.json +74 -37
- package/src/index.ts +5 -0
- package/src/lib/GoogleSpreadsheet.ts +636 -0
- package/src/lib/GoogleSpreadsheetCell.ts +307 -0
- package/src/lib/GoogleSpreadsheetCellErrorValue.ts +25 -0
- package/src/lib/GoogleSpreadsheetRow.ts +117 -0
- package/{lib/GoogleSpreadsheetWorksheet.js → src/lib/GoogleSpreadsheetWorksheet.ts} +271 -178
- package/src/lib/lodash.ts +32 -0
- package/src/lib/types/auth-types.ts +18 -0
- package/src/lib/types/drive-types.ts +40 -0
- package/src/lib/types/sheets-types.ts +528 -0
- package/src/lib/types/util-types.ts +8 -0
- package/src/lib/utils.ts +57 -0
- package/.eslintrc.js +0 -61
- package/.nvmrc +0 -1
- package/Changelog.md +0 -10
- package/TODO +0 -73
- package/UNLICENSE +0 -24
- package/index.js +0 -13
- package/lib/GoogleSpreadsheet.js +0 -426
- package/lib/GoogleSpreadsheetCell.js +0 -239
- package/lib/GoogleSpreadsheetRow.js +0 -72
- package/lib/errors.js +0 -10
- package/lib/utils.js +0 -32
|
@@ -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
|
+
}
|