UNPKG

mini-sheets

Version:
524 lines (501 loc) 17.2 kB
const {google} = require("googleapis"); function firstKey(obj){ return Object.keys(obj)[0]; } function empty(obj){ return Object.keys(obj).length === 0; } function validate(originalObj, verifyObj) { for (let key in verifyObj) { if (originalObj[key] !== undefined) { verifyObj[key] = originalObj[key]; } } return verifyObj; } function parseable(str){ try { return JSON.parse(str); } catch(e) { return false; } } class SingleMetadata { constructor(key, value, id, parentMetadata) { this.key = key; this.value = value; if (this.value !== undefined && typeof this.value === 'string') { if (typeof parseable(this.value) === 'object') this.value = JSON.parse(this.value); else if (this.value === 'true' || this.value === 'false') this.value = JSON.parse(this.value); else if (!isNaN(Number(this.value))) this.value = Number(this.value); } this.id = id; this.parent = parentMetadata; } toSingleMetadataObject(){ let temp = { metadataKey: this.key, metadataValue: typeof this.value === 'object' ? JSON.stringify(this.value) : String(this.value), visibility: 'DOCUMENT', }; if (this.parent.path) temp.location = {sheetId: this.parent.path}; if (this.id) temp.metadataId = this.id; return temp; } equals(singleMetadata){ for (let key in this) { if (key === 'parent') continue; if (this[key] !== singleMetadata[key]) return false; } return true; } } class Metadata { constructor(sheetMetaPath = null, sheetMetaPairs = {}) { this.path = sheetMetaPath; this.data = {}; for (let metaKey in sheetMetaPairs) { this.data[metaKey] = new SingleMetadata(metaKey, sheetMetaPairs[metaKey], null, this); } } static parse(sheetObj) { let parsedSheetMeta = new Metadata(); parsedSheetMeta.path = sheetObj.properties.sheetId; if (!sheetObj.developerMetadata) return parsedSheetMeta; for (let i = 0; i < sheetObj.developerMetadata.length; i++) { let meta = sheetObj.developerMetadata[i]; parsedSheetMeta.data[meta.metadataKey] = new SingleMetadata(meta.metadataKey, meta.metadataValue, meta.metadataId, parsedSheetMeta); } return parsedSheetMeta; } fillEmpty(oldMetadata){ if (!this.path) this.path = oldMetadata.path; for (let metaKey in this.data) { if (oldMetadata.data[metaKey]) { this.data[metaKey].id = this.data[metaKey].id || oldMetadata.data[metaKey].id; } } for (let metaKey in oldMetadata.data) { if (!this.data.hasOwnProperty(metaKey)) this.data[metaKey] = new SingleMetadata(metaKey, oldMetadata.data[metaKey].value, oldMetadata.data[metaKey].id, this); } } toMetadataObject(){ let obj = []; for (let metaKey in this.data) { if (this.data[metaKey].value !== undefined) obj.push(this.data[metaKey].toSingleMetadataObject()); } return obj; } } class Sheet { constructor(sheetTitle, sheetGridData) { this.sheetId = null; this.sheetTitle = sheetTitle; if (sheetGridData === null) { this.data = null; return this; } if (sheetGridData === true) sheetGridData = [[]]; sheetGridData = sheetGridData || [[]]; if (!(sheetGridData instanceof Array)) throw new Error('Grid data must be an array'); if (!sheetGridData[0]) throw new Error('Incomplete grid data'); let columnCount = sheetGridData[0].length; for (let i = 1; i < sheetGridData.length; i++) { if (sheetGridData[i].length !== columnCount) throw new Error('Column count does not match per row'); } this.rows = sheetGridData.length || 1; this.columns = sheetGridData[0].length || 1; this.data = sheetGridData; } static parse(sheetObj) { let parsedSheet = new Sheet(); parsedSheet.sheetId = sheetObj.properties.sheetId; parsedSheet.sheetTitle = sheetObj.properties.title; parsedSheet.rows = sheetObj.properties.gridProperties.rowCount; parsedSheet.columns = sheetObj.properties.gridProperties.columnCount; parsedSheet.data = []; if (sheetObj.data && sheetObj.data[0].rowData) { let sheetData = sheetObj.data[0].rowData; for (let i = 0; i < sheetData.length; i++) { let sheetRow = sheetData[i].values; let row = []; for (let k = 0; k < sheetRow.length; k++) { if (empty(sheetRow[k])) { row.push(null); continue; } let type = firstKey(sheetRow[k].effectiveValue || sheetRow[k].userEnteredValue); let value = (sheetRow[k].effectiveValue) ? sheetRow[k].effectiveValue[type] : sheetRow[k].userEnteredValue[type]; switch(type) { case 'numberValue': row.push(Number(value)); break; case 'boolValue': row.push(!!value); break; default: row.push(value); break; } } parsedSheet.data.push(row); } } return parsedSheet; } fillEmpty(oldSheet = {}){ if (oldSheet.sheetId) this.sheetId = oldSheet.sheetId; } toSheetObject(metadata = {data: {}}){ let obj = { properties: { title: this.sheetTitle, gridProperties: { rowCount: this.rows, columnCount: this.columns } }, data: [{ rowData: [], startRow: 0, startColumn: 0, }], developerMetadata: [], } if (this.sheetId) { obj.properties.sheetId = this.sheetId; } for (let i = 0; i < this.data.length; i++) { let singleRow = { values: [] } for (let k = 0; k < this.data[i].length; k++) { let gridValue = this.data[i][k]; switch (typeof gridValue) { case 'string': singleRow.values.push({userEnteredValue: {stringValue: gridValue}}); break; case 'number': singleRow.values.push({userEnteredValue: {numberValue: gridValue}}); break; case 'boolean': singleRow.values.push({userEnteredValue: {boolValue: gridValue}}); break; default: singleRow.values.push({}); break; } } obj.data[0].rowData.push(singleRow); } if (empty(metadata.data)) delete obj.developerMetadata; else obj.developerMetadata = metadata.toMetadataObject(); return obj; } } class Worksheet { constructor(worksheetObj = {spreadsheetId: null, properties: {title: null}, sheets: []}) { this.worksheetId = worksheetObj.spreadsheetId; this.worksheetTitle = worksheetObj.properties.title; this.maxId = 0; this.sheets = {}; this.metadata = {}; for (let i = 0; i < worksheetObj.sheets.length; i++) { let title = worksheetObj.sheets[i].properties.title; this.sheets[title] = Sheet.parse(worksheetObj.sheets[i]); this.maxId = Math.max(this.sheets[title].sheetId, this.maxId); this.metadata[title] = Metadata.parse(worksheetObj.sheets[i]); } } simplify(){ let obj = { title: this.worksheetTitle, sheets: {}, metadata: {}, id: this.worksheetId } for (let sheetTitle in this.sheets) { obj.sheets[sheetTitle] = this.sheets[sheetTitle].data; } for (let sheetTitle in this.metadata) { if (!this.metadata[sheetTitle] || empty(this.metadata[sheetTitle].data)) continue; obj.metadata[sheetTitle] = {}; for (let metaKey in this.metadata[sheetTitle].data) { obj.metadata[sheetTitle][metaKey] = this.metadata[sheetTitle].data[metaKey].value; } } return obj; } static create(title, gridData = {}, metadata = {}){ if (!title) throw new Error('Missing Spreadsheet Title'); if (typeof gridData !== 'object') throw new TypeError('gridData must be an object'); if (typeof metadata !== 'object') throw new TypeError('metaData must be an object'); let generatedWorksheet = new Worksheet(); generatedWorksheet.worksheetTitle = title; for (let sheetTitle in gridData) { generatedWorksheet.sheets[sheetTitle] = new Sheet(sheetTitle, gridData[sheetTitle]); this.maxId = Math.max(this.maxId, generatedWorksheet.sheets[sheetTitle].sheetId); } for (let sheetTitle in metadata) { generatedWorksheet.metadata[sheetTitle] = {}; for (let metaTitle in metadata[sheetTitle]) { generatedWorksheet.metadata[sheetTitle] = new Metadata(null, metadata[sheetTitle]); } } return generatedWorksheet; } toSpreadsheetObject(){ let obj = { properties: { title: this.worksheetTitle }, } if (this.worksheetId) obj.spreadsheetId = this.worksheetId; if (!empty(this.sheets)) obj.sheets = []; for (let sheetTitle in this.sheets) { obj.sheets.push(this.sheets[sheetTitle].toSheetObject(this.metadata[sheetTitle])); } return obj } createSheetId(){ this.maxId++; return this.maxId; } } class gAPI { constructor(client_id, token){ let client_secret; if (typeof client_id === 'object') { client_secret = client_id.client_secret; client_id = client_id.client_id; } else if (typeof client_id !== 'string') { throw new TypeError('Invalid client ID or client object') } if (typeof token !== 'object') throw new TypeError('Token must be an object'); this.oauth = new google.auth.OAuth2(client_id, client_secret); this.oauth.setCredentials(token); } } class Drive extends gAPI { constructor(client_id, token) { super(client_id, token); this.drive = google.drive({version: "v3", auth: this.oauth}).files; } getFile(fileId) { if (fileId === undefined) throw new Error('Must include file ID'); if (typeof fileId !== 'string') throw new TypeError('File ID must be a string'); return new Promise((resolve, reject)=>{ this.drive.get({fileId: fileId, fields: '*'}, (err, res)=>{ if (err) { if (err.errors && err.errors[0].reason === 'notFound') { console.warn('\x1b[33m%s\x1b[0m', `File: '${fileId}' Not Found`); resolve(null); } else reject(err); } else resolve(res.data); }); }); } setFile(fileId, properties = {}) { if (fileId === undefined) throw new Error('Must include file ID'); if (typeof fileId !== 'string') throw new TypeError('File ID must be a string'); let request = { fileId: fileId, resource: properties, fields: '*' }; if (empty(properties)) { console.warn('\x1b[33m%s\x1b[0m', 'Empty properties object'); } return new Promise((resolve, reject)=>{ this.drive.update(request, (err, res)=>{ if (err) { if (err.errors[0].reason === 'notFound') { console.warn('\x1b[33m%s\x1b[0m', `File: '${fileId}' Not Found`); resolve(null); } else reject(err); } else resolve(res.data); }); }); } async deleteFile(fileId) { if (fileId === undefined) throw new Error('Must include file ID'); if (typeof fileId !== 'string') throw new TypeError('File ID must be a string'); return await new Promise((resolve, reject)=>{ this.drive.delete({fileId: fileId}, (err, res)=>{ if (err) { if (err.errors && err.errors[0].reason === 'notFound') { console.warn('\x1b[33m%s\x1b[0m', `File: '${fileId}' Not Found`); resolve(false); } else reject(err); } else resolve(true); }); }); } } class Spreadsheets extends gAPI { constructor(client_id, token) { super(client_id, token); this.spreadsheets = google.sheets({version: "v4", auth: this.oauth}).spreadsheets; } createSpreadsheet(title, gridData, metadata) { title = title || "Untitled Spreadsheet"; if (typeof title !== 'string') throw new TypeError('Spreadsheet title must be a string'); return new Promise((resolve, reject)=>{ this.spreadsheets.create({resource: Worksheet.create(title, gridData, metadata).toSpreadsheetObject()}, (err, res)=>{ if (err) reject(err); else resolve(new Worksheet(res.data).simplify()); }); }); } getRawSpreadsheet(spreadsheetId) { if (spreadsheetId === undefined) throw new Error('Must include spreadsheet ID'); if (typeof spreadsheetId !== 'string') throw new TypeError('Spreadsheet ID must be a string'); return new Promise((resolve, reject)=>{ this.spreadsheets.get({spreadsheetId: spreadsheetId, includeGridData: false}, (err, res)=>{ if (err) reject(err); else resolve(new Worksheet(res.data)); }); }); } async getSpreadsheet(spreadsheetId, _options = {}){ if (spreadsheetId === undefined) throw new Error('Must include spreadsheet ID'); if (typeof spreadsheetId !== 'string') throw new TypeError('Spreadsheet ID must be a string'); if (typeof _options !== 'object') throw new TypeError('Options must be an object'); _options = validate(_options, {include: []}); if (!(_options.include instanceof Array)) _options.include = [_options.include]; if (_options.include.length > 0) { let preSpreadsheet = await this.getRawSpreadsheet(spreadsheetId); return await new Promise((resolve, reject)=>{ let dataFilters = []; for (let i = 0; i < _options.include.length; i++) { if (!preSpreadsheet.sheets[_options.include]) return reject(new Error('Sheet name does not exist')); dataFilters.push({gridRange: {sheetId: preSpreadsheet.sheets[_options.include].sheetId}}); } this.spreadsheets.getByDataFilter({ spreadsheetId: spreadsheetId, resource: { dataFilters: dataFilters, includeGridData: true } }, (err, res)=>{ if (err) reject(err); else resolve(new Worksheet(res.data).simplify()); }); }); } else { return await new Promise((resolve, reject)=>{ this.spreadsheets.get({ spreadsheetId: spreadsheetId, includeGridData: true }, (err, res)=>{ if (err) reject(err); else resolve(new Worksheet(res.data).simplify()); }); }); } } async setSpreadsheet(spreadsheetId, gridData, metadata){ if (spreadsheetId === undefined) throw new Error('Must include spreadsheet ID'); if (typeof spreadsheetId !== 'string') throw new TypeError('Spreadsheet ID must be a string'); let preSpreadsheet = await this.getRawSpreadsheet(spreadsheetId), newSpreadsheet = Worksheet.create(preSpreadsheet.worksheetTitle, gridData, metadata); let requests = []; let sheetTitles = {}; for (let key in newSpreadsheet.sheets) sheetTitles[key] = true; for (let key in newSpreadsheet.metadata) sheetTitles[key] = true; for (let sheetTitle in sheetTitles) { let newConvertedSheet, oldConvertedSheet = {developerMetadata: []}, newSheet = newSpreadsheet.sheets[sheetTitle], preSheet = preSpreadsheet.sheets[sheetTitle], newMeta = newSpreadsheet.metadata[sheetTitle], preMeta = preSpreadsheet.metadata[sheetTitle] || {data: {}}; if(newSheet) newSheet.fillEmpty(preSheet); if (newSheet.data === null) { if (newSheet.sheetId) { requests.push({ deleteSheet: { sheetId: newSheet.sheetId } }); } continue; } if(newMeta) newMeta.fillEmpty(preMeta); if (newSheet && !preSheet) { let tempId = preSpreadsheet.createSheetId(); newSheet.sheetId = tempId; requests.push({ addSheet: { properties: newSheet.toSheetObject().properties } }); } if (newMeta) { for (let metaKey in newMeta.data) { if (newSheet && !preSheet) { newMeta.data[metaKey].parent.path = newSheet.sheetId; } if (newMeta.data[metaKey].value === undefined && newMeta.data[metaKey].id) { requests.push({ deleteDeveloperMetadata: { dataFilter: { developerMetadataLookup: { metadataId: newMeta.data[metaKey].id } } } }) } else if (!preMeta.data[metaKey]) { requests.push({ createDeveloperMetadata: { developerMetadata: newMeta.data[metaKey].toSingleMetadataObject() } }); } else if (!newMeta.data[metaKey].equals(preMeta.data[metaKey])) { requests.push({ updateDeveloperMetadata: { fields: '*', dataFilters: [{ developerMetadataLookup: { metadataId: newMeta.data[metaKey].id } }], developerMetadata: newMeta.data[metaKey].toSingleMetadataObject() } }); } } } if ((newSheet && preSheet) && (newSheet.rows !== preSheet.rows || newSheet.columns !== preSheet.columns)) { requests.push({ updateSheetProperties: { fields: '*', properties: newSheet.toSheetObject().properties } }); } if (!empty(gridData)) { requests.push({ updateCells: { fields: '*', rows: newSheet.toSheetObject().data[0].rowData, range: { sheetId: newSheet.toSheetObject().properties.sheetId, startRowIndex: 0, startColumnIndex: 0, } } }); } } if (requests.length === 0) return null; return await new Promise((resolve, reject)=>{ this.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, resource: { requests: requests, includeSpreadsheetInResponse: true, responseIncludeGridData: true } }, (err, res)=>{ if (err) reject(err); else resolve(new Worksheet(res.data.updatedSpreadsheet).simplify()); }); }); return requests; } } module.exports = { Drive: Drive, Spreadsheets: Spreadsheets };