Bằng cách viết trình phân giải tuỳ chỉnh, bạn có thể mở rộng Firebase SQL 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 cung cấp) thành một truy vấn hoặc đột biến.
Khái niệm "nguồn dữ liệu" rất linh hoạt. Nguồn dữ liệu 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 hoạt động tích hợp dựa trên API, chẳng hạn như Stripe, SendGrid, Salesforce và các dịch vụ khác.
- Logic nghiệp vụ tuỳ chỉnh.
Sau khi bạn viết trình phân giải tuỳ chỉnh để hỗ trợ các nguồn dữ liệu bổ sung, các SQL Connect truy vấn và đột biến có thể kết hợp các nguồn dữ liệu này 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. 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 máy khách an toàn theo kiểu cho web, Android và iOS.
- Các truy vấn trả về dữ liệu từ nhiều nguồn.
- Các lệnh gọi hàm bị hạn chế dựa trên trạng thái cơ sở dữ liệu.
Đ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 điều 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 hàm bằng Cloud Functions cho Firebase. Đây là cách bạn sẽ triển khai logic của trình phân giải tuỳ chỉnh.
Trước khi bắt đầu
Bạn phải thiết lập một dự án để sử dụng SQL 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
Ở cấp độ cao, việc viết trình phân giải tuỳ chỉnh có 3 phần: đầu tiên là xác định giản đồ cho trình phân giải tuỳ chỉnh; thứ hai là triển khai trình phân giải bằng Cloud Functions; và cuối cùng là 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 cho 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ẽ trình bày 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 SQL Connect.
Xác định giản đồ cho trình phân giải tuỳ chỉnh
Trong thư mục dự án Firebase, hãy chạy:
firebase init dataconnect:resolverFirebase CLI sẽ nhắc bạn nhập tên cho trình phân giải tuỳ chỉnh và hỏi xem có tạo các cách 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ụ sẽ tạo một tệp
dataconnect/schema_resolver/schema.gqltrống và thêm cấu hình trình phân giải mới vào tệpdataconnect.yaml.Cập nhật tệp
schema.gqlnày bằng lược đồ GraphQL xác định các truy vấn và đột biến mà trình phân giải tuỳ chỉnh sẽ cung cấp. Ví dụ: đây là 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 trình phân giải bằng Cloud Functions. Trong nội bộ, 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 là 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 trình phân giải truy cập vào nguồn dữ liệu.
Mở tệp
functions/src/index.ts.Khi bạn chạy
firebase init dataconnect:resolverở trên, lệnh này đã tạo thư mục mã nguồn Cloud Functions này và khởi chạy thư mục đó bằng mã mẫu trongindex.ts.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);
Các cách triển khai khung này cho thấy hình dạng chung mà hàm trình phân giải phải có. Để tạo trình phân giải tuỳ chỉnh hoạt động đầy đủ, bạn 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.
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à bạn đã 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 SQL Connect truy vấn và đột biến. Sau đó, bạn sẽ sử dụng các thao tác này để tự động tạo SDK máy khách tuỳ chỉnh mà bạn có thể dùng để truy cập vào tất cả dữ liệu, cho dù được hỗ trợ bởi Cloud SQL, trình phân giải tuỳ chỉnh hay sự kết hợp.
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.
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 } }Đột biến 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. Xin lưu ý rằng giản đồ sử dụng SQL Connect's
@authchỉ thị để đảm bảo rằng người dùng có thể chỉ cập nhật hồ sơ của riêng họ. Vì bạn đang truy cập vào kho dữ liệu thông qua SQL Connect, nên bạn có thể tự động tận dụng các tính năng của SQL Connect như tính năng này.
Trong các ví dụ ở trên, bạn đã xác định các thao tác SQL Connect truy cập vào dữ liệu từ kho dữ liệu bằng 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 vào 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 một số trường hợp sử dụng nâng cao hơn kết hợp dữ liệu từ nhiều nguồn.
Trước đó, hãy tiếp tục phần tiếp theo để xem trình phân giải tuỳ chỉnh của bạn hoạt động.
Triển khai trình phân giải và 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 đồ SQL Connect, bạn phải triển khai các giản đồ đó để 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 functionsBây giờ, bạn có thể triển khai các lược đồ và thao tác đã cập nhật:
firebase deploy --only dataconnectSau khi thay đổi lược đồ SQL Connect, bạn cũng phải tạo SDK máy khách mới:
firebase dataconnect:sdk:generateVí dụ
Các 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 trình phân giải tuỳ chỉnh bằ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 SQL Connect bằ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 xây dựng 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 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 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 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 là một vectơ tiềm ẩn cho hành vi lạm dụng, nên bạn muốn đảm bảo rằng người nhận dự định đã 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 truy vấn Cloud SQL trước để đả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 sử 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 nguồn dữ liệu trong bối cảnh của 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 độ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 đối với một nguồn dữ liệu hoàn tất trước khi đưa ra yêu cầu đối với một nguồn dữ liệu khác. Ví dụ: giả sử bạn có một truy vấn tự động chuyển ngữ một video theo yêu cầu bằng API AI. Lệnh gọi API như thế này có thể tốn kém, vì vậy, bạn muốn kiểm soát lệnh gọi này dựa trên 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 số loại tín dụng cao cấp trong ứng dụng của bạn.
Lần thử đầu tiên để đạt được điều này có thể trô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á khả năng đồng thời. Mặt khác, các trường của độ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 độ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. Hãy lưu ý các hạn chế hiện tại sau:
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 trình phân giải tuỳ chỉnh. Ví dụ: bạn không thể thực hiện như 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 chuẩn (ví dụ: $authUid) và xác thực các biến đó ở cấp độ thao tác bằ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 thay thế khác là di chuyển tất cả logic vào 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 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 SDK quản trị cho cơ sở dữ liệu 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 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 đầ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ể đứng trước các thao tác SQL
Trong một đột biến, việc đặt trình phân giải tuỳ chỉnh trước các thao tác SQL 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 (@transaction)
Không thể gói trình phân giải tuỳ chỉnh bên trong khối @transaction bằng các thao tác SQL chuẩn. Nếu Cloud Function hỗ trợ trình phân giải không thành công sau khi 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 của giao dịch giữa SQL và một nguồn dữ liệu khác, hãy di chuyển logic thao tác SQL vào bên trong Cloud Functions và xử lý quá trình xác thực và khôi phục bằng SDK dành cho quản trị viên hoặc kết nối SQL trực tiếp.