搭配 SQL Connect 使用 Admin SDK

Firebase Admin SDK 是一組伺服器程式庫,可讓您從具特殊權限的環境與 Firebase 互動,執行查詢和變更 Firebase SQL Connect 服務等動作,以便進行大量資料管理,以及其他需要進階權限和模擬憑證的操作。

Admin SDK 提供 API,可在讀取/寫入和唯讀模式下呼叫作業。有了唯讀作業,您就能安心實作管理功能,不必擔心資料庫中的資料遭到修改。

Admin SDK 設定

如要在伺服器上開始使用 Firebase SQL Connect,您必須先安裝並設定 Node.js 的 Admin SDK。

在指令碼中初始化 Admin SDK

如要初始化 SDK,請匯入 SQL Connect 擴充功能,並宣告專案服務 ID 和位置。


import { initializeApp } from 'firebase-admin/app';
import { getDataConnect } from 'firebase-admin/data-connect';

// If you'd like to use OAuth2 flows and other credentials to log in,
// visit https://firebase.google.com/docs/admin/setup#initialize-sdk
// for alternative ways to initialize the SDK.

const app = initializeApp();

const dataConnect = getDataConnect({
    serviceId: 'serviceId',
    location: 'us-west2'
});

設計要與 Admin SDK 搭配使用的查詢和異動

考量到下列事項,Admin SDK 適合用於執行 SQL Connect 作業。

瞭解 SDK 和 @auth(level: NO_ACCESS) 作業指令

由於 Admin SDK 具有權限,因此無論使用 @auth 指令設定的存取層級為何 (包括 NO_ACCESS 層級),都能執行任何查詢和變更。

除了用戶端作業外,如果您還將管理查詢和變異整理到 .gql 來源檔案中,以便匯入管理指令碼,Firebase 建議您將管理作業標示為不具任何授權存取層級,或更明確地將其設為 NO_ACCESS。無論採用哪種方式,都能防止這類作業在用戶端或其他非特殊權限環境中執行。

搭配使用 SDK 和 SQL Connect 模擬器

在原型和測試環境中,對本機資料執行資料播種和其他作業可能很有幫助。Admin SDK 可忽略本機流程的驗證和授權,簡化工作流程。(您也可以明確選擇加入遵守作業的驗證和授權設定,並使用使用者身分模擬功能)。

設定 DATA_CONNECT_EMULATOR_HOST 環境變數後,Firebase Admin SDK 會自動連線至 SQL Connect 模擬器:

export DATA_CONNECT_EMULATOR_HOST="127.0.0.1:9399"

詳情請參閱:

執行管理員作業

Admin SDK適用於重要資料的特殊權限作業。

Admin SDK 提供三組 API:

  • 產生的 Admin SDK,這是從 gql 定義產生的類型安全 SDK,產生方式與產生用戶端 SDK 相同。
  • 用於執行任意 GraphQL 作業的一般介面,您的程式碼會實作查詢和異動,並將其傳遞至讀寫 executeGraphql 方法或唯讀 executeGraphqlRead 方法。
  • 這是專門用於大量資料作業的介面,不使用一般 executeGraphql 方法,而是提供專門用於變更作業的方法:insert、insertMany、upsert 和 upsertMany。

使用生成的 SDK 管理資料

您可以從 gql 定義產生 Admin SDK,方法與產生用戶端 SDK 相同。

產生的 Admin SDK 包含與 gql 定義對應的介面和函式,可用於對資料庫執行作業。舉例來說,假設您為歌曲資料庫生成 SDK,並附上查詢 getSongs:

import { initializeApp } from "firebase-admin/app";
import { getSongs } from "@dataconnect/admin-generated";

const adminApp = initializeApp();

const songs = await getSongs(
  { limit: 4 },
  { impersonate: { unauthenticated: true } }
);

或者,如要指定連接器設定,請按照下列步驟操作:

import { initializeApp } from "firebase-admin/app";
import { getDataConnect } from "firebase-admin/data-connect";
import {
  connectorConfig,
  getSongs,
} from "@dataconnect/admin-generated";

const adminApp = initializeApp();
const adminDc = getDataConnect(connectorConfig);

const songs = await getSongs(
  adminDc,
  { limit: 4 },
  { impersonate: { unauthenticated: true } }
);

冒用未經驗證的使用者

Admin SDK 應在受信任的環境中執行,因此可不受限制地存取資料庫。

使用 Admin SDK 執行公開作業時,請避免以完整管理員權限執行作業 (遵循最小權限原則)。您應該改為以模擬使用者 (請參閱下一節) 或模擬未經驗證的使用者身分執行作業。未經驗證的使用者只能執行標示為 PUBLIC 的作業。

在上述範例中,getSongs 查詢是以未經驗證的使用者身分執行。

模擬使用者

您也可以在 impersonate 選項中傳遞部分或全部 Firebase Authentication 權杖,代表特定使用者執行作業;您至少必須在 sub 宣告中指定使用者的使用者 ID。(這個值與您可在 SQL Connect GraphQL 作業中參照的 auth.uid 伺服器值相同)。

模擬使用者時,只有在您提供的使用者資料通過 GraphQL 定義中指定的身分驗證檢查時,作業才會成功。

如果您是從可公開存取的端點呼叫產生的 SDK,請務必確保端點需要驗證,且您必須先驗證驗證權杖的完整性,才能使用該權杖模擬使用者。

使用可呼叫的 Cloud Functions 時,系統會自動驗證驗證權杖,您可以按照下列範例使用:

import { HttpsError, onCall } from "firebase-functions/https";

export const callableExample = onCall(async (req) => {
    const authClaims = req.auth?.token;
    if (!authClaims) {
        throw new HttpsError("unauthenticated", "Unauthorized");
    }

    const favoriteSongs = await getMyFavoriteSongs(
        undefined,
        { impersonate: { authClaims } }
    );

    // ...
});

否則,請使用 Admin SDK 的 verifyIdToken 方法驗證及解碼驗證權杖。舉例來說,假設您的端點是以一般 HTTP 函式實作,且您已使用 authorization 標頭將 Firebase Authentication 權杖傳遞至端點,這是標準做法:

import { getAuth } from "firebase-admin/auth";
import { onRequest } from "firebase-functions/https";

const auth = getAuth();

export const httpExample = onRequest(async (req, res) => {
    const token = req.header("authorization")?.replace(/^bearer\s+/i, "");
    if (!token) {
        res.sendStatus(401);
        return;
    }
    let authClaims;
    try {
        authClaims = await auth.verifyIdToken(token);
    } catch {
        res.sendStatus(401);
        return;
    }

    const favoriteSongs = await getMyFavoriteSongs(
        undefined,
        { impersonate: { authClaims } }
    );

    // ...
});

只有在安全、非公開存取的環境中執行真正的管理工作 (例如資料遷移) 時,才應指定並非來自可驗證來源的使用者 ID:

// Never do this if end users can initiate execution of the code!
const favoriteSongs = await getMyFavoriteSongs(
  undefined,
  { impersonate: { authClaims } }
);

執行時不受存取限制

如果您執行的作業需要管理員層級權限,請從呼叫中省略模擬參數:

await upsertSong(adminDc, {
  title: songTitle_one,
  instrumentsUsed: [Instrument.VOCAL],
});

以這種方式呼叫的作業可以完整存取資料庫。如果查詢或變異僅用於管理目的,請使用 @auth(level: NO_ACCESS) 指令定義。這樣做可確保只有管理員層級的呼叫端可以執行這些作業。

使用 executeGraphql 方法管理資料

如需執行未定義 gql 異動或查詢的一次性作業,可以使用 executeGraphql 方法或唯讀 executeGraphqlRead 方法。

使用 executeGraphql 執行自訂巢狀關係運算 (一對多和一對一)

在具特殊權限的環境中,從 Admin SDK 呼叫作業時,您不需要在變數上指定 @allow 指令,即可進行標準平面 (非巢狀) 插入 (例如插入個別電影或演員記錄)。

不過,如果是巢狀關聯插入 (例如插入電影及其初始評論,或使用者及其個人資料),您仍須在變數定義中宣告 @allow 指令,以靜態方式允許編譯變數結構定義中的巢狀關聯欄位。

// Custom GraphQL mutation with nested relational insert (one-to-many)
const customMutation = `
  mutation CustomBatchInsert(
    $data: [Movie_Data!]!
    @allow(fields: "title genre reviews_on_movie { rating reviewText }")
  ) {
    movie_insertMany(data: $data)
  }
`;
const variables = {
  data: [
    {
      title: "Interstellar",
      genre: "Sci-Fi",
      reviews_on_movie: [
        {
          rating: 5,
          reviewText: "Visually stunning and emotionally powerful.",
          user: { id: "user-789" }
        }
      ]
    }
  ]
};
const response = await dataConnect.executeGraphql(customMutation, { variables });
// Custom GraphQL mutation with a one-to-one nested relational insert
const customMutationOneToOne = `
  mutation CustomBatchInsertOneToOne(
    $data: [User_Data!]!
    @allow(fields: "name profile_on_user { bio }")
  ) {
    user_insertMany(data: $data)
  }
`;
const variablesOneToOne = {
  data: [
    {
      name: "Alice",
      profile_on_user: {
        bio: "Avid moviegoer and critic."
      }
    }
  ]
};
const responseOneToOne = await dataConnect.executeGraphql(customMutationOneToOne, { variables: variablesOneToOne });

冒用未經驗證的使用者

使用 Admin SDK 執行公開作業時,請避免以完整管理員權限執行作業 (遵循最小權限原則)。建議您改以模擬使用者 (請參閱下一節) 或模擬未經驗證的使用者身分執行作業。未經驗證的使用者只能執行標示為 PUBLIC 的作業。

// Query to get posts, with authentication level PUBLIC
const queryGetPostsImpersonation = `
    query getPosts @auth(level: PUBLIC) {
        posts {
          description
        }
    }`;

// Attempt to access data as an unauthenticated user
const optionsUnauthenticated: GraphqlOptions<undefined> = {
    impersonate: {
        unauthenticated: true
    }
};

// executeGraphql with impersonated unauthenticated user scope
const gqlResponse = await dataConnect.executeGraphql<UserData, undefined>(queryGetPostsImpersonation, optionsUnauthenticated);

模擬使用者

在某些情況下,您可能希望指令碼能代表特定使用者,根據有限的憑證修改使用者資料。這種做法符合最小權限原則。

如要使用這個介面,請從符合 Authentication 權杖格式的自訂 JWT 驗證權杖中收集資訊。另請參閱自訂權杖指南。

// Get the current user's data
const queryGetUserImpersonation = `
    query getUser @auth(level: USER) {
        user(key: {uid_expr: "auth.uid"}) {
            id,
            name
        }
    }`;

// Impersonate a user with the specified auth claims
const optionsAuthenticated: GraphqlOptions<undefined> = {
    impersonate: {
        authClaims: {
            sub: 'QVBJcy5ndXJ1'
        }
    }
};

// executeGraphql with impersonated authenticated user scope
const gqlResponse = await dataConnect.executeGraphql<UserData, undefined>(queryGetUserImpersonation, optionsAuthenticated);

// gqlResponse -> { "data": { "user": { "id": "QVBJcy5ndXJ1", "name": "Fred" } } }

使用管理員憑證

如果您執行的作業需要管理員層級權限,請從呼叫中省略模擬參數:

// User can be publicly accessible, or restricted to admins
const query = "query getProfile(id: AuthID) { user(id: $id) { id name } }";

interface UserData {
  user: {
    id: string;
    name: string;
  };
}

export interface UserVariables {
  id: string;
}

const options:GraphqlOptions<UserVariables> = { variables: { id: "QVBJcy5ndXJ1" } };

// executeGraphql
const gqlResponse = await dataConnect.executeGraphql<UserData, UserVariables>(query, options);

// executeGraphqlRead (similar to previous sample but only for read operations)
const gqlResponse = await dataConnect.executeGraphqlRead<UserData, UserVariables>(query, options);

// gqlResponse -> { "data": { "user": { "id": "QVBJcy5ndXJ1", "name": "Fred" } } }

以這種方式呼叫的作業可以完整存取資料庫。如果查詢或變異僅用於管理目的,請使用 @auth(level: NO_ACCESS) 指令定義。這樣做可確保只有管理員層級的呼叫端可以執行這些作業。

執行大量資料插入作業

Firebase 建議您使用 Admin SDK,對正式資料庫執行大量資料作業。

SDK 提供下列方法,可用於處理大量資料。每個方法都會根據提供的引數,建構並執行 GraphQL 變異。


// Methods of the bulk operations API
// dc is a SQL Connect admin instance from getDataConnect

const resp = await dc.insert("movie" /*table name*/, data[0]);
const resp = await dc.insertMany("movie" /*table name*/, data);
const resp = await dc.upsert("movie" /*table name*/, data[0]);
const resp = await dc.upsertMany("movie" /*table name*/, data);

巢狀關係運算 (一對多和一對一)

Admin SDK 支援大量作業中的關聯資料。與自訂異動相同,請使用物件陣列建立一對多關係,並使用單一物件建立一對一關係:

// Example of inserting a movie with nested reviews (one-to-many)
const moviesData = [
  {
    title: "Interstellar",
    genre: "Sci-Fi",
    reviews_on_movie: [
      {
        rating: 5,
        reviewText: "Visually stunning and emotionally powerful.",
        user: { id: "user-789" }
      }
    ]
  }
];

// Atomically insert movies and their reviews in a single database round-trip.
const response = await dc.insertMany("movie", moviesData);
// Example of inserting a user with a nested profile (one-to-one)
const usersData = [
  {
    name: "Alice",
    profile_on_user: {
      bio: "Avid moviegoer and critic."
    }
  }
];

// Atomically insert users and their profiles.
const userResponse = await dc.insertMany("user", usersData);

大量作業的效能注意事項

每次向後端發出要求,都會產生一次往返 Cloud SQL 的作業,因此批次處理的數量越多,處理量就越高。

不過,批次大小越大,產生的 SQL 陳述式就越長。達到 PostgreSQL SQL 陳述式長度限制時,就會發生錯誤。

在實際操作中,請嘗試找出適合工作負載的批次大小。

後續步驟