Mở rộng Data Connect bằng các trình phân giải tuỳ chỉnh

Bằng cách viết các trình phân giải tuỳ chỉnh, bạn có thể mở rộng Firebase Data Connect để hỗ trợ các nguồn dữ liệu khác ngoài Cloud SQL. Sau đó, bạn có thể kết hợp nhiều nguồn dữ liệu (Cloud SQL và các nguồn dữ liệu do trình phân giải tuỳ chỉnh của bạn cung cấp) thành một truy vấn hoặc đột biến duy nhất.

Khái niệm về "nguồn dữ liệu" rất linh hoạt. Thư viện này bao gồm:

  • Các cơ sở dữ liệu khác ngoài Cloud SQL, chẳng hạn như Cloud Firestore, MongoDB và các cơ sở dữ liệu khác.
  • Các dịch vụ lưu trữ như Cloud Storage, AWS S3 và các dịch vụ khác.
  • Mọi chế độ tích hợp dựa trên API, chẳng hạn như Stripe, SendGrid, Salesforce và các chế độ khác.
  • Logic nghiệp vụ tuỳ chỉnh.

Sau khi bạn viết các trình phân giải tuỳ chỉnh để hỗ trợ các nguồn dữ liệu bổ sung, các truy vấn và đột biến Data Connect có thể kết hợp chúng theo nhiều cách, mang lại những lợi ích như:

  • Một lớp uỷ quyền hợp nhất cho các nguồn dữ liệu của bạn. Ví dụ: cho phép truy cập vào các tệp trong Cloud Storage bằng dữ liệu được lưu trữ trong Cloud SQL.
  • SDK phía máy khách an toàn về kiểu cho web, Android và iOS.
  • Các truy vấn trả về dữ liệu từ nhiều nguồn.
  • Hạn chế lệnh gọi hàm dựa trên trạng thái cơ sở dữ liệu của bạn.

Điều kiện tiên quyết

Để viết trình phân giải tuỳ chỉnh của riêng mình, bạn cần có những thông tin sau:

  • Firebase CLI phiên bản 15.9.0 trở lên
  • SDK Hàm Firebase phiên bản 7.1.0 trở lên

Ngoài ra, bạn nên làm quen với việc viết các hàm bằng Cloud Functions cho Firebase. Đây là cách bạn sẽ triển khai logic của các trình phân giải tuỳ chỉnh.

Trước khi bắt đầu

Bạn nên thiết lập một dự án để sử dụng Data Connect.

Bạn có thể làm theo một trong các hướng dẫn Bắt đầu nhanh để thiết lập nếu chưa thiết lập:

Viết trình phân giải tuỳ chỉnh

Nhìn chung, việc viết một trình phân giải tuỳ chỉnh có 3 phần: thứ nhất, xác định một lược đồ cho trình phân giải tuỳ chỉnh; thứ hai, triển khai trình phân giải bằng Cloud Functions; và cuối cùng, sử dụng các trường trình phân giải tuỳ chỉnh trong các truy vấn và đột biến, có thể kết hợp với Cloud SQL hoặc các trình phân giải tuỳ chỉnh khác.

Hãy làm theo các bước trong vài phần tiếp theo để tìm hiểu cách thực hiện. Ví dụ, giả sử bạn có thông tin hồ sơ công khai của người dùng được lưu trữ bên ngoài Cloud SQL. Kho dữ liệu chính xác không được chỉ định trong các ví dụ này, nhưng có thể là một thứ gì đó như Cloud Storage, một phiên bản MongoDB hoặc bất kỳ thứ gì khác.

Các phần sau đây sẽ minh hoạ một cách triển khai khung của trình phân giải tuỳ chỉnh có thể đưa thông tin hồ sơ bên ngoài đó vào Data Connect.

Xác định giản đồ cho trình phân giải tuỳ chỉnh

  1. Trong thư mục dự án Firebase, hãy chạy:

    firebase init dataconnect:resolver

    Firebase CLI sẽ nhắc bạn nhập tên cho trình phân giải tuỳ chỉnh và hỏi xem bạn có muốn tạo các triển khai trình phân giải mẫu bằng TypeScript hoặc JavaScript hay không. Nếu bạn đang làm theo hướng dẫn này, hãy chấp nhận tên mặc định và tạo các ví dụ về TypeScript.

    Sau đó, công cụ này sẽ tạo một tệp dataconnect/schema_resolver/schema.gql trống và thêm cấu hình trình phân giải mới vào tệp dataconnect.yaml.

  2. Cập nhật tệp schema.gql này bằng một giản đồ GraphQL xác định các truy vấn và đột biến mà trình phân giải tuỳ chỉnh của bạn sẽ cung cấp. Ví dụ: sau đây là một giản đồ cho trình phân giải tuỳ chỉnh có thể truy xuất và cập nhật hồ sơ công khai của người dùng, được lưu trữ trong một kho dữ liệu khác ngoài Cloud SQL:

    # dataconnect/schema_resolver/schema.gql
    
    type PublicProfile {
      name: String!
      photoUrl: String!
      bioLine: String!
    }
    
    type Query {
      # This field will be backed by your Cloud Function.
      publicProfile(userId: String!): PublicProfile
    }
    
    type Mutation {
      # This field will be backed by your Cloud Function.
      updatePublicProfile(
        userId: String!, name: String, photoUrl: String, bioLine: String
      ): PublicProfile
    }
    

Triển khai logic trình phân giải tuỳ chỉnh

Tiếp theo, hãy triển khai các trình phân giải bằng Cloud Functions. Trong quá trình này, bạn sẽ tạo một máy chủ GraphQL; tuy nhiên, Cloud Functions có một phương thức trợ giúp onGraphRequest xử lý các chi tiết của việc này, vì vậy, bạn chỉ cần viết logic của trình phân giải để truy cập vào nguồn dữ liệu của mình.

  1. Mở tệp functions/src/index.ts.

    Khi bạn chạy firebase init dataconnect:resolver ở trên, lệnh này sẽ tạo thư mục mã nguồn Cloud Functions và khởi động thư mục này bằng mã mẫu trong index.ts.

  2. Thêm các định nghĩa sau:

    import {
      FirebaseContext,
      onGraphRequest,
    } from "firebase-functions/dataconnect/graphql";
    
    const opts = {
      // Points to the schema you defined earlier, relative to the root of your
      // Firebase project.
      schemaFilePath: "dataconnect/schema_resolver/schema.gql",
      resolvers: {
        query: {
          // This resolver function populates the data for the "publicProfile" field
          // defined in your GraphQL schema located at schemaFilePath.
          publicProfile(
            _parent: unknown,
            args: Record<string, unknown>,
            _contextValue: FirebaseContext,
            _info: unknown
          ) {
            const userId = args.userId;
    
            // Here you would use the user ID to retrieve the user profile from your data
            // store. In this example, we just return a hard-coded value.
    
            return {
              name: "Ulysses von Userberg",
              photoUrl: "https://example.com/profiles/12345/photo.jpg",
              bioLine: "Just a guy on a mountain. Ski fanatic.",
            };
          },
        },
        mutation: {
          // This resolver function updates data for the "updatePublicProfile" field
          // defined in your GraphQL schema located at schemaFilePath.
          updatePublicProfile(
            _parent: unknown,
            args: Record<string, unknown>,
            _contextValue: FirebaseContext,
            _info: unknown
          ) {
            const { userId, name, photoUrl, bioLine } = args;
    
            // Here you would update in your datastore the user's profile using the
            // arguments that were passed. In this example, we just return the profile
            // as though the operation had been successful.
    
            return { name, photoUrl, bioLine };
          },
        },
      },
    };
    
    export const resolver = onGraphRequest(opts);
    

Những cách triển khai khung này cho thấy hình dạng chung mà một trình phân giải phải có. Để tạo một trình phân giải tuỳ chỉnh hoạt động đầy đủ, bạn sẽ cần điền vào các phần được nhận xét bằng mã đọc và ghi vào nguồn dữ liệu của mình.

Sử dụng trình phân giải tuỳ chỉnh trong các truy vấn và đột biến

Bây giờ, bạn đã xác định giản đồ của trình phân giải tuỳ chỉnh và đã triển khai logic hỗ trợ giản đồ đó, bạn có thể sử dụng trình phân giải tuỳ chỉnh trong các truy vấn và đột biến Data Connect. Sau đó, bạn sẽ sử dụng các thao tác này để tự động tạo một SDK ứng dụng tuỳ chỉnh mà bạn có thể dùng để truy cập vào tất cả dữ liệu của mình, cho dù được hỗ trợ bởi Cloud SQL, các trình phân giải tuỳ chỉnh hay một tổ hợp.

  1. Trong dataconnect/example/queries.gql, hãy thêm định nghĩa sau:

    query GetPublicProfile($id: String!)
        @auth(level: PUBLIC, insecureReason: "Anyone can see a public profile.") {
      publicProfile(userId: $id) {
        name
        photoUrl
        bioLine
      }
    }
    

    Truy vấn này truy xuất hồ sơ công khai của người dùng bằng trình phân giải tuỳ chỉnh của bạn.

  2. Trong dataconnect/example/mutations.gql, hãy thêm định nghĩa sau:

    mutation SetPublicProfile(
      $id: String!, $name: String, $photoUrl: String, $bioLine: String
    ) @auth(expr: "vars.id == auth.uid") {
      updatePublicProfile(userId: $id, name: $name, photoUrl: $photoUrl, bioLine: $bioLine) {
        name
        photoUrl
        bioLine
      }
    }
    

    Thao tác sửa đổi này ghi một tập hợp dữ liệu hồ sơ mới vào kho dữ liệu, một lần nữa sử dụng trình phân giải tuỳ chỉnh của bạn. Xin lưu ý rằng giản đồ này sử dụng chỉ thị @auth của Data Connect để đảm bảo rằng người dùng chỉ có thể cập nhật hồ sơ của riêng họ. Vì đang truy cập vào kho dữ liệu thông qua Data Connect, nên bạn có thể tự động tận dụng các tính năng của Data Connect, chẳng hạn như tính năng này.

Trong các ví dụ trên, bạn đã xác định các thao tác Data Connect truy cập vào dữ liệu từ kho dữ liệu bằng cách sử dụng các trình phân giải tuỳ chỉnh. Tuy nhiên, bạn không bị giới hạn trong các thao tác truy cập dữ liệu từ Cloud SQL hoặc từ một nguồn dữ liệu tuỳ chỉnh duy nhất. Hãy xem phần Ví dụ để biết thêm một số trường hợp sử dụng nâng cao kết hợp dữ liệu từ nhiều nguồn.

Trước đó, hãy tiếp tục chuyển sang phần tiếp theo để xem các trình phân giải tuỳ chỉnh của bạn đang hoạt động.

Triển khai trình phân giải và các thao tác tuỳ chỉnh

Giống như khi thực hiện bất kỳ thay đổi nào đối với giản đồ Data Connect, bạn phải triển khai giản đồ để các thay đổi có hiệu lực. Trước khi thực hiện, trước tiên hãy triển khai logic trình phân giải tuỳ chỉnh mà bạn đã triển khai bằng Cloud Functions:

firebase deploy --only functions

Giờ đây, bạn có thể triển khai các lược đồ và thao tác đã cập nhật:

firebase deploy --only dataconnect

Sau khi thay đổi giản đồ Data Connect, bạn cũng phải tạo SDK ứng dụng mới:

firebase dataconnect:sdk:generate

Ví dụ

Những ví dụ này cho thấy cách triển khai một số trường hợp sử dụng nâng cao hơn và cách tránh những cạm bẫy thường gặp.

Cho phép truy cập vào một trình phân giải tuỳ chỉnh bằng cách sử dụng dữ liệu từ Cloud SQL

Một trong những lợi ích của việc tích hợp các nguồn dữ liệu vào Data Connect bằng cách sử dụng trình phân giải tuỳ chỉnh là bạn có thể viết các thao tác kết hợp các nguồn dữ liệu.

Trong ví dụ này, giả sử bạn đang tạo một ứng dụng mạng xã hội và bạn đã triển khai một đột biến dưới dạng một trình phân giải tuỳ chỉnh, gửi email nhắc nhở cho bạn bè của người dùng nếu họ không tương tác với người dùng trong một khoảng thời gian.

Để triển khai tính năng nhắc nhở, hãy tạo một trình phân giải tuỳ chỉnh có giản đồ như sau:

# A GraphQL server must define a root query type per the spec.
type Query {
  unused: String
}

type Mutation {
  sendEmail(id: String!, content: String): Boolean
}

Định nghĩa này được hỗ trợ bởi một Cloud Function, chẳng hạn như sau:

import {
  FirebaseContext,
  onGraphRequest,
} from "firebase-functions/dataconnect/graphql";

const opts = {
  schemaFilePath: "dataconnect/schema_resolver/schema.gql",
  resolvers: {
    mutation: {
      sendEmail(
        _parent: unknown,
        args: Record<string, unknown>,
        _contextValue: FirebaseContext,
        _info: unknown
      ) {
        const { id, content } = args;

        // Look up the friend's email address and call the cloud service of your
        // choice to send the friend an email with the given content.

       return true;
      },
    },
  },
};

export const resolver = onGraphRequest(opts);

Vì việc gửi email vừa tốn kém cho bạn vừa có thể là một phương thức lạm dụng, nên bạn cần đảm bảo rằng người nhận dự kiến đã có trong danh sách bạn bè của người dùng trước khi sử dụng trình phân giải tuỳ chỉnh sendEmail.

Giả sử trong ứng dụng của bạn, dữ liệu danh sách bạn bè được lưu trữ trong Cloud SQL:

type User @table {
  id: String! @default(expr: "auth.uid")
  acceptNudges: Boolean! @default(value: false)
}

type UserFriend @table(key: ["user", "friend"]) {
  user: User!
  friend: User!
}

Bạn có thể viết một đột biến trước tiên truy vấn Cloud SQL để đảm bảo rằng người gửi có trong danh sách bạn bè của người nhận trước khi dùng trình phân giải tuỳ chỉnh để gửi email:

# Send a "nudge" to a friend as a reminder. This will only let the user send a
# nudge if $friendId is in the user's friends list.
mutation SendNudge($friendId: String!) @auth(level: USER_EMAIL_VERIFIED) {
  # Step 1: Query and check
  query @redact {
    userFriend(
      key: {userId_expr: "auth.uid", friendId: $friendId}
    # This checks that $friendId is in the user's friends list.
    ) @check(expr: "this != null", message: "You must be friends to nudge") {
      friend {
        # This checks that the friend is accepting nudges.
        acceptNudges @check(expr: "this == true", message: "Not accepting nudges")
      }
    }
  }
  # Step 2: Act
  sendEmail(id: $friendId, content: "You've been nudged!")
}

Ngoài ra, ví dụ này cũng minh hoạ rằng một nguồn dữ liệu trong bối cảnh của các trình phân giải tuỳ chỉnh có thể bao gồm các tài nguyên khác ngoài cơ sở dữ liệu và các hệ thống tương tự. Trong ví dụ này, nguồn dữ liệu là một dịch vụ gửi email trên đám mây.

Đảm bảo thực thi tuần tự bằng cách sử dụng các đột biến

Khi kết hợp các nguồn dữ liệu, bạn thường cần đảm bảo rằng một yêu cầu đến một nguồn dữ liệu hoàn tất trước khi đưa ra yêu cầu đến một nguồn dữ liệu khác. Ví dụ: giả sử bạn có một truy vấn phiên âm động một video theo yêu cầu bằng cách sử dụng một API AI. Một lệnh gọi API như thế này có thể tốn kém, vì vậy, bạn nên kiểm soát lệnh gọi theo một số tiêu chí, chẳng hạn như người dùng sở hữu video hoặc người dùng đã mua một loại tín dụng cao cấp nào đó trong ứng dụng của bạn.

Lần đầu tiên thử đạt được điều này có thể có dạng như sau:

# This won't work as expected.
query BrokenTranscribeVideo($videoId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  # Step 1: Check quota using SQL.
  # Verify the user owns the video and has "pro" status or credits.
  checkQuota: query @redact {
    video(id: $videoId)
    {
      user @check(expr: "this.id == auth.uid && this.hasCredits == true", message: "Unauthorized access") {
        id
        hasCredits
      }
    }
  }

  # Step 2: Trigger expensive compute
  # Only triggers if Step 1 succeeds? No! This won't work because query field
  # execution order is not guaranteed.
  triggerTranscription: query {
    # For example, might call Vertex AI or Transcoder API.
    startVideoTranscription(videoId: $videoId)
  }
}

Phương pháp này sẽ không hoạt động vì không đảm bảo thứ tự thực thi của các trường truy vấn; máy chủ GraphQL dự kiến có thể phân giải các trường theo bất kỳ thứ tự nào để tối đa hoá tính đồng thời. Mặt khác, các trường của một đột biến luôn được phân giải theo thứ tự, vì máy chủ GraphQL dự kiến rằng một số trường của một đột biến có thể có tác dụng phụ khi được phân giải.

Mặc dù bước đầu tiên của thao tác ví dụ không có tác dụng phụ, nhưng bạn có thể xác định thao tác này là một đột biến để tận dụng thực tế là các trường đột biến được phân giải theo thứ tự:

# By using a mutation, we guarantee the SQL check happens FIRST.
mutation TranscribeVideo($videoId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  # Step 1: Check quota using SQL.
  # Verify the user owns the video and has "pro" status or credits.
  checkQuota: query @redact {
    video(id: $videoId)
    {
      user @check(expr: "this.id == auth.uid && this.hasCredits == true", message: "Unauthorized access") {
        id
        hasCredits
      }
    }
  }

  # Step 2: Trigger expensive compute
  # This Cloud Function will ONLY trigger if Step 1 succeeds.
  triggerTranscription: query {
    # For example, might call Vertex AI or Transcoder API.
    startVideoTranscription(videoId: $videoId)
  }
}

Hạn chế

Tính năng trình phân giải tuỳ chỉnh được phát hành dưới dạng bản dùng thử công khai thử nghiệm. Lưu ý các hạn chế hiện tại sau đây:

Không có biểu thức CEL trong đối số trình phân giải tuỳ chỉnh

Bạn không thể sử dụng biểu thức CEL một cách linh hoạt trong các đối số cho một trình phân giải tuỳ chỉnh. Ví dụ: Bạn không thể làm những việc sau:

mutation UpdateMyProfile($newName: String!) @auth(level: USER) {
  updateMongoDocument(
    collection: "profiles"
    # This isn't supported:
    id_expr: "auth.uid"
    update: { name: $newName }
  )
}

Thay vào đó, hãy truyền các biến tiêu chuẩn (ví dụ: $authUid) và xác thực các biến đó ở cấp hoạt động bằng cách sử dụng chỉ thị @auth(expr: ...) được đánh giá an toàn.

mutation UpdateMyProfile(
  $newName: String!, $authUid: String!
) @auth(expr: "vars.authUid == auth.uid") {
  updateMongoDocument(
    collection: "profiles"
    id: $authUid
    update: { name: $newName }
  )
}

Một giải pháp khác là di chuyển tất cả logic của bạn vào một trình phân giải tuỳ chỉnh và hoàn tất tất cả các thao tác dữ liệu của bạn từ Cloud Functions.

Ví dụ: hãy xem xét ví dụ này, hiện sẽ không hoạt động:

mutation BrokenForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  query {
    chatMessage(id: $chatMessageId) {
      content
    }
  }
  sendEmail(
    title: "Forwarded Chat Message"
    to_expr: "auth.token.email" # Not supported.
    content_expr: "response.query.chatMessage.content" # Not supported.
  )
}

Thay vào đó, hãy di chuyển cả truy vấn Cloud SQL và lệnh gọi đến dịch vụ email vào một trường đột biến, được hỗ trợ bởi một hàm:

mutation ForwardToEmail($chatMessageId: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
  forwardChatToEmail(
    chatMessageId: $chatMessageId
  )
}

Tạo một SDK quản trị cho cơ sở dữ liệu của bạn và sử dụng SDK đó trong hàm để thực hiện truy vấn Cloud SQL:

const opts = {
 schemaFilePath: "dataconnect/schema_resolver/schema.gql",
 resolvers: {
   query: {
     async forwardToEmail(
       _parent: unknown,
       args: Record<string, unknown>,
       _contextValue: FirebaseContext,
       _info: unknown
     ) {
       const chatMessageId = args.chatMessageId as string;

       let decodedToken;
       try {
         decodedToken = await getAuth().verifyIdToken(_contextValue.auth.token ?? "");
       } catch (error) {
         return false;
       }

       const email = decodedToken.email;
       if (!email) {
         return false;
       }

       const response = await getChatMessage({chatMessageId});
       const messageContent = response.data.chatMessage?.content;

       // Here you call the cloud service of your choice to send the email with
       // the message content.

       return true;
     }
   },
 },
};
export const resolver = onGraphRequest(opts);

Không có loại đối tượng đầu vào trong các tham số trình phân giải tuỳ chỉnh

Trình phân giải tuỳ chỉnh không chấp nhận các loại dữ liệu đầu vào GraphQL phức tạp. Các tham số phải là các loại vô hướng cơ bản (String, Int, Date, Any, v.v.) và Enum.

input PublicProfileInput {
  name: String!
  photoUrl: String!
  bioLine: String!
}

type Mutation {
  # Not supported:
  updatePublicProfile(userId: String!, profile: PublicProfileInput): PublicProfile

  # OK:
  updatePublicProfile(userId: String!, name: String, photoUrl: String, bioLine: String): PublicProfile
}

Trình phân giải tuỳ chỉnh không thể đi trước các thao tác SQL

Trong một đột biến, việc đặt một trình phân giải tuỳ chỉnh trước các thao tác SQL tiêu chuẩn sẽ dẫn đến lỗi. Tất cả các thao tác dựa trên SQL phải xuất hiện trước mọi lệnh gọi trình phân giải tuỳ chỉnh.

Không có giao dịch nào (@transaction)

Bạn không thể bao bọc các trình phân giải tuỳ chỉnh bên trong khối @transaction bằng các thao tác SQL tiêu chuẩn. Nếu Cloud Function hỗ trợ trình phân giải gặp lỗi sau khi lệnh chèn SQL thành công, thì cơ sở dữ liệu sẽ không tự động khôi phục.

Để đạt được tính an toàn giao dịch giữa SQL và một nguồn dữ liệu khác, hãy di chuyển logic hoạt động SQL vào bên trong Cloud Function, đồng thời xử lý việc xác thực và khôi phục bằng Admin SDK hoặc các kết nối SQL trực tiếp.