星期三, 4月 01, 2026

[GAS] Realtime Database

透過 Google Apps Script 存取 Firebase Realtime Database (NoSQL)

Firebase 上建立新專案

建立過程依序會詢問專案名稱、是否在 Firebase 上啟用 Gemini、是否啟用 Google Analytics

建立 Firebase RealTime Database

產品類別 => 資料庫和儲存空間 => NoSQL => Realtime Database
點選建立資料庫
選擇資料庫位置,選擇新加玻 (asia-southeadt1)
安全性規則,預設為鎖定模式
Realtime Database 建立完成,紅線為 Readtime Database URL,GAS 會透過它來存取

訂閱方案 - Spark Plan

使用雲端服務最重要的了解帳單費用,Spark Plan 訂閱是 free 的,但 Realtime Database 有使用量限制,萬一到達使用量限制服務就會停止,下圖為 Realtime Database 使用限制,關於 Spark Plan 其他服務使用限制,可以參考官方網站 - Pricing
Google Apps Script CRUD

從 GAS 連線至 Realtime Database 是透過 ScriptApp.getOAuthToken() 取的個人身份來進行驗證,該筆記只著重在 Realtime Database CRUD 操作是沒有問題,但萬一該存取 Realtime Database 會開放,改用 GAS 第三方套件 OAuth2 搭配 Firebase 服務帳號來使用會比較適合

設定 oauth 授權範圍,Google Apps Script Project settings => 啟用 [在編輯器中顯示「appsscript.json」資訊清單檔案]

在編輯器中出現「appsscript.json」資訊清單檔案並加入 oauth 授權範圍

appsscript.json 完整資訊,oauthScopes 為手動加入
{
  "timeZone": "Asia/Taipei",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
      "https://www.googleapis.com/auth/script.external_request",
      "https://www.googleapis.com/auth/userinfo.email",
      "https://www.googleapis.com/auth/firebase.database"
    ]  
}

在指令碼指令內建立 Realtime Database URL
CRUD Code
/**
 * 從 Script Properties 取得 Firebase 資料庫網址
 * @constant {string}
 */
const FIREBASE_URL = PropertiesService.getScriptProperties().getProperty("FIREBASE_URL");

/**
 * 寫入或覆寫資料 (PUT)
 * 執行此操作會將指定路徑下的資料完全替換為傳入的 data。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @param {Object} data - 欲寫入或覆寫的 JSON 資料物件
 * @returns {Object} Firebase 伺服器回傳的 JSON 解析物件
 */
function writeDataBuiltIn(path, data) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;

  const options = {
    method: "put", // 使用 put 進行資料寫入
    contentType: "application/json",
    headers: {
      Authorization: 'Bearer ' + token
    },
    payload: JSON.stringify(data),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return JSON.parse(response.getContentText());
}

/**
 * 讀取資料 (GET)
 * 從指定的資料庫路徑讀取目前的資料。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @returns {Object|null} Firebase 伺服器回傳的 JSON 解析物件(若該路徑無資料則為 null)
 */
function readDataBuiltIn(path) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;
  
  const options = {
    method: "get", // 使用 get 進行存取
    headers: {
      Authorization: 'Bearer ' + token
    },
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return JSON.parse(response.getContentText());
}

/**
 * 部分更新資料 (PATCH)
 * 只會你傳入的欄位,不會覆寫原本存在的其他欄位。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @param {Object} data - 要更新的 JSON 資料物件
 * @returns {Object} Firebase 伺服器回傳的 JSON 解析物件(包含已更新的欄位資料)
 */
function updateDataBuiltIn(path, data) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;

  const options = {
    method: "patch", // 使用 patch 進行部分更新
    contentType: "application/json",
    headers: {
      Authorization: 'Bearer ' + token
    },
    payload: JSON.stringify(data),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return JSON.parse(response.getContentText());
}

/**
 * 刪除資料 (DELETE)
 * 會刪除該路徑下的所有資料與子節點。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @returns {string} Firebase 伺服器回傳的回應字串(刪除成功通常會回傳 "null" 字串)
 */
function deleteDataBuiltIn(path) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;

  const options = {
    method: "delete", // 使用 delete 刪除節點
    headers: {
      Authorization: 'Bearer ' + token
    },
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return response.getContentText(); 
}
實際測試

寫入
function testWriteDataBuiltIn() {
  const userPath = "customers/user_123";
  const userData = {
    name: "陳大文",
    email: "david@example.com",
    membership: "VIP",
    joinDate: Utilities.formatDate(new Date() , Session.getScriptTimeZone() , 'yyyy-MM-dd HH:mm:ss')
  };
    
  writeDataBuiltIn(userPath, userData);
  console.log("寫入測試完成!請到 Firebase 後台查看結果。");
}
讀取

function testReadDataBuiltIn() {

  const testPath = "customers/user_123";   
  const data = readDataBuiltIn(testPath);
  
  if (data !== null) {
    console.log("讀取成功!資料內容如下:");
    console.log(JSON.stringify(data, null, 2));
  } else {
    console.log("讀取失敗,請檢查權限或路徑。");
  }
}
更新

function testUpdateDataBuiltIn() {

  const userPath = "customers/user_123";
  
  // 假設原本資料有 name, email, membership。
  // 我們現在只想升級他的 membership,並新增一個 points 欄位,保留原本的 name 與 email。
  const updatePayload = {
    membership: "VVIP", // 更新現有欄位
    points: 500,        // 新增原本沒有的欄位
    lastUpdated: Utilities.formatDate(new Date() , Session.getScriptTimeZone() , 'yyyy-MM-dd HH:mm:ss')
  };
  
  updateDataBuiltIn(userPath, updatePayload);
  console.log("更新完成!請去 Firebase 檢查 user_123 的資料,原本的名字應該還在。");
}
刪除
function testDeleteDataBuiltIn() {
  const userPath = "customers/user_123";
  deleteDataBuiltIn(userPath);
  console.log("刪除完成!該節點已經從 Firebase 移除了。");
}


沒有留言:

張貼留言