Skip to content

Instantly share code, notes, and snippets.

@schlosser
Last active January 10, 2022 03:42
Show Gist options
  • Save schlosser/7417a3362eaafe43dbad1c42d8d21d84 to your computer and use it in GitHub Desktop.
Save schlosser/7417a3362eaafe43dbad1c42d8d21d84 to your computer and use it in GitHub Desktop.
Copy your Google Sheets spreadsheet to Firebase using this AppScript.
// Installation & Setup
// 1. Create a spreadsheet.
// 2. Under Tools > Script Editor, paste this script
// 3. Under Resources > Libraries, add two library dependencies:
// - OAuth2: 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
// - FirebaseApp: 1hguuh4Zx72XVC1Zldm_vTtcUUKUA6iBUOoGnJUWLfqDWx5WlOJHqYkrt
// 4. Create a Firebase project and initialize the Realtime Database
// 5. Fill in your project details in FIREBASE_IO_URL and SERVICE_ACCOUNT below.
// 6. Under Resources > Cloud Platform Project, add your Firebase project's project number from GCP. You
// can find your project number in the GCP Cloud Console here: https://console.cloud.google.com/home/dashboard?project=_
// 7. Give it a whirl from your Google Sheet! Note: you may need to click past security warnings.
// TODO: Add your Firebase project ID here. It will look something like "my-project-123"
VAR FIREBASE_IO_URL = 'https://YOUR-PROJECT-ID.firebaseio.com'
// TODO: Add your Firebase service account here.
// Generate one by clicking "Generate new private Key" here: https://console.firebase.google.com/u/0/project/_/settings/serviceaccounts
var SERVICE_ACCOUNT = {
"type": "service_account",
"project_id": "XXX-XXX-XXXX",
"private_key_id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"private_key": "-----BEGIN PRIVATE KEY-----\XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n-----END PRIVATE KEY-----\n",
"client_email": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"client_id": "01234567890123456789",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
};
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Publish Website')
.addItem('Publish this sheet', 'publishSheet')
.addItem('Publish all sheets', 'publishAllSheets')
.addToUi();
}
function publishSheet() {
var sheet = SpreadsheetApp.getActiveSheet();
copySheetDataToFirebase(sheet);
}
function publishAllSheets() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var idx = 0; idx < sheets.length; idx++) {
copySheetDataToFirebase(sheets[idx]);
}
}
function copySheetDataToFirebase(sheet) {
var logMessage = 'Publishing "' + sheet.getName() + '".';
SpreadsheetApp.getActiveSpreadsheet().toast(logMessage, "My Website");
var normalizedSheetName = normalizeHeader(sheet.getName());
var headersRange = sheet.getRange(1,1,1,sheet.getMaxColumns());
var [rows, columns] = [sheet.getLastRow(), sheet.getLastColumn()]
var range = sheet.getRange(2,1,rows,columns);
var data = getRowsData(sheet, range, 1);
var service = getFirebaseService();
if (service.hasAccess()) {
var fb = FirebaseApp.getDatabaseByUrl(FIREBASE_IO_URL, service.getAccessToken())
fb.setData(normalizedSheetName + '/', data);
} else {
Logger.log(service.getLastError());
}
}
function reset() {
var service = getFirebaseService();
service.reset();
}
function getFirebaseService() {
return OAuth2.createService('Firebase')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(SERVICE_ACCOUNT.private_key)
.setIssuer(SERVICE_ACCOUNT.client_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setScope('https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/firebase.database');
}
// Copied from https://gist.github.com/mhawksey/1442370
// getRowsData iterates row by row in the input range and returns an array of objects.
// Each object contains all the data for a given row, indexed by its normalized column name.
// Arguments:
// - sheet: the sheet object that contains the data to be processed
// - range: the exact range of cells where the data is stored
// - columnHeadersRowIndex: specifies the row number where the column names are stored.
// This argument is optional and it defaults to the row immediately above range;
// Returns an Array of objects.
function getRowsData(sheet, range, columnHeadersRowIndex) {
columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;
var numColumns = range.getEndColumn() - range.getColumn() + 1;
var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);
var headers = headersRange.getValues()[0];
return getObjects(range.getValues(), normalizeHeaders(headers));
}
// For every row of data in data, generates an object that contains the data. Names of
// object fields are defined in keys.
// Arguments:
// - data: JavaScript 2d array
// - keys: Array of Strings that define the property names for the objects to create
function getObjects(data, keys) {
var objects = [];
for (var i = 0; i < data.length; ++i) {
var object = {};
var hasData = false;
for (var j = 0; j < data[i].length; ++j) {
var key = keys[j];
var cellData = data[i][j];
if (isCellEmpty(cellData)) {
continue;
}
// Clean up column-specific things
if (key in dataCleaners) {
cellData = dataCleaners[key](cellData);
}
// Format dates
if (cellData.toISOString) {
cellData = cellData.toISOString();
}
object[key] = escapeHtml(cellData);
hasData = true;
}
if (hasData) {
objects.push(object);
}
}
return objects;
}
// Returns an Array of normalized Strings.
// Arguments:
// - headers: Array of Strings to normalize
function normalizeHeaders(headers) {
var keys = [];
for (var i = 0; i < headers.length; ++i) {
var key = normalizeHeader(headers[i], true); // alphanumeric
if (key.length > 0) {
keys.push(key);
}
}
return keys;
}
// Normalizes a string, by removing all alphanumeric characters and using mixed case
// to separate words. The output will always start with a lower case letter.
// This function is designed to produce JavaScript object property names.
// Arguments:
// - header: string to normalize
// Examples:
// "First Name" -> "firstName"
// "Market Cap (millions) -> "marketCapMillions
// "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored"
function normalizeHeader(header, alphanumeric) {
var key = "";
var upperCase = false;
for (var i = 0; i < header.length; ++i) {
var letter = header[i];
if (letter == " " && key.length > 0) {
upperCase = true;
continue;
}
if (key.length == 0 && isDigit(letter)) {
continue; // first character must be a letter
}
if (upperCase) {
upperCase = false;
key += letter.toUpperCase();
} else {
key += letter.toLowerCase();
}
}
if (alphanumeric) {
key = key.replace(/\W/g,"");
}
return key;
}
var dataCleaners = {
"email": function(email) {
return email.toLowerCase();
},
"phoneNumber": function(phoneNumber) {
// Strip everything except numbers and the + sign.
return phoneNumber.toString().replace(/[^+\d]*/g, "");
},
}
// Returns true if the cell where cellData was read from is empty.
// Arguments:
// - cellData: string
function isCellEmpty(cellData) {
return typeof(cellData) == "string" && cellData == "";
}
// Returns true if the character char is alphabetical, false otherwise.
function isAlnum(char) {
return char >= 'A' && char <= 'Z' ||
char >= 'a' && char <= 'z' ||
isDigit(char);
}
// Returns true if the character char is a digit, false otherwise.
function isDigit(char) {
return char >= '0' && char <= '9';
}
// Escape any double-quotes in the string.
function escapeHtml (string) {
return String(string).replace(new RegExp('"', 'g'), '\"');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment