@resolute/gsheet
Version:
Simple abstractions for Google Sheets
214 lines (212 loc) • 6.94 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
exports.gsheet =
exports.Gsheet =
exports.noFilter =
exports.noChange =
exports.trim =
exports.gSheetDateTime =
exports.gSheetDate =
void 0;
/* eslint-disable camelcase */
/* eslint-disable no-use-before-define */
const sheets_1 = require('@googleapis/sheets');
const promise_1 = require('@resolute/std/promise');
const gSheetDate = (date = new Date()) =>
`${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
exports.gSheetDate = gSheetDate;
const gSheetDateTime = (date = new Date()) =>
`${(0, exports.gSheetDate)(date)} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
exports.gSheetDateTime = gSheetDateTime;
const trim = (arg) => {
if (typeof arg === 'string') {
return arg.replace(/\s+/g, ' ').trim();
}
return arg;
};
exports.trim = trim;
const noChange = (arg) => arg;
exports.noChange = noChange;
const noFilter = () => true;
exports.noFilter = noFilter;
// From Google’s API (TypeScript/Go To Definition) documentation:
//
// For output, empty trailing rows and columns will not be included.
//
// For input, supported value types are: bool, string, and double. Null values
// will be skipped. To set a cell to an empty value, set the string value to an
// empty string.
class Gsheet {
constructor(options) {
let _a; let _b; let _c; let
_d;
this.auth = (() => {
const googleAuth = new sheets_1.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
if (options.jwt) {
const client_email =
'client_email' in options.jwt ? options.jwt.client_email : options.jwt.email;
const private_key =
'private_key' in options.jwt ? options.jwt.private_key : options.jwt.key;
return googleAuth.fromJSON({ client_email, private_key });
}
return googleAuth.getClient();
})();
this.client = Promise.resolve(this.auth).then((auth) => {
let _a;
return (0, sheets_1.sheets)({
version: 'v4',
auth,
http2: (_a = options.http2) !== null && _a !== void 0 ? _a : false,
});
});
this.spreadsheetId = options.spreadsheetId;
this.range = options.range;
this.headerRows =
(_a = options === null || options === void 0 ? void 0 : options.headerRows) !== null &&
_a !== void 0
? _a
: 1;
this.keyTransform =
(_b = options === null || options === void 0 ? void 0 : options.keyTransform) !== null &&
_b !== void 0
? _b
: exports.noChange;
this.sanitize =
(_c = options === null || options === void 0 ? void 0 : options.sanitize) !== null &&
_c !== void 0
? _c
: exports.trim;
this.filter =
(_d = options === null || options === void 0 ? void 0 : options.filter) !== null &&
_d !== void 0
? _d
: exports.noFilter;
this.keptSheet = (0, promise_1.keeper)(this.getSheet.bind(this));
this.keptColumns = (0, promise_1.keeper)(this.getColumns.bind(this));
if (options.preload) {
this.refresh();
}
if (options.interval) {
this.keepFresh(options.interval);
}
}
async getSheet(range = this.range) {
// const auth = new google.auth.GoogleAuth();
const client = await this.client;
const response = await client.spreadsheets.values.get({
spreadsheetId: this.spreadsheetId,
range,
});
if (!response.data.values) {
throw new Error('No data found.');
}
return response.data.values;
}
getHeaderRowsOnly() {
return this.getSheet(`${this.range}!A${this.headerRows}:ZZZ${this.headerRows}`);
}
getColumnsFromSheetCache() {
try {
return this.keptSheet.stale();
} catch (_a) {
return this.getHeaderRowsOnly();
}
}
async getColumns() {
const rows = await this.getColumnsFromSheetCache();
const columns = rows[this.headerRows - 1];
if (!columns || !columns.length) {
throw new Error(`Unable to turn rows into objects. Row at ${this.headerRows} is empty.`);
}
return columns.map(this.sanitize).map(this.keyTransform);
}
async columns() {
return this.keptColumns.get();
}
async rows() {
const rows = await this.keptSheet.get();
return rows
.slice(this.headerRows)
.map((row) => row.map(this.sanitize))
.filter(this.filter);
}
async data() {
// IMPORTANT: Get the rows first--otherwise, if the cache is empty,
// this.columns() will do its own expensive call to get only the header
// rows, even though we’re going to need all of the rows immediately after.
const rows = await this.rows();
const keys = await this.columns();
return rows
.map((row) =>
row.reduce((obj, value, index) => {
if (keys[index]) {
// eslint-disable-next-line no-param-reassign
obj[keys[index]] = value;
}
return obj;
}, {}))
.filter(this.filter);
}
async normalizeInputData(arg) {
if (Array.isArray(arg)) {
return arg;
}
// match against lowercase
const entries = Object.entries(arg).map(([key, val]) => [key.toLowerCase(), val]);
const columns = await this.columns();
return columns
.map((key) => (entries.find(([entryKey]) => entryKey === key.toLowerCase()) || [])[1])
.map(this.sanitize);
}
async append(arg) {
let _a; let
_b;
const row = await this.normalizeInputData(arg);
if (!row) {
throw new Error(`Unable to add ${arg}.`);
}
const client = await this.client;
const response = await client.spreadsheets.values.append({
spreadsheetId: this.spreadsheetId,
range: this.range,
insertDataOption: 'INSERT_ROWS',
valueInputOption: 'USER_ENTERED',
requestBody: { range: this.range, values: [row] },
});
// TODO: should we do this? Or allow other readers to still get stale content?
this.keptSheet.fresh();
if (
!(
((_b =
(_a = response === null || response === void 0 ? void 0 : response.data) === null ||
_a === void 0
? void 0
: _a.updates) === null || _b === void 0
? void 0
: _b.updatedRows) > 0
)
) {
throw new Error('Failed to save your information. Please try again.');
}
return response;
}
refresh() {
this.keptSheet.fresh();
}
keepFresh(interval) {
this.keptSheet.start(interval);
}
}
exports.Gsheet = Gsheet;
const gsheet = (options) => {
const instance = new Gsheet(options);
instance.rows = instance.rows.bind(instance);
instance.data = instance.data.bind(instance);
instance.keepFresh = instance.keepFresh.bind(instance);
instance.append = instance.append.bind(instance);
instance.refresh = instance.refresh.bind(instance);
return instance;
};
exports.gsheet = gsheet;
exports.default = exports.gsheet;