1. Trước khi bắt đầu
Trong lớp học lập trình này, bạn sẽ tích hợp Firebase SQL Connect với cơ sở dữ liệu Cloud SQL để tạo Friendly Exchange, một ứng dụng web thị trường chứng khoán biểu tượng cảm xúc theo thời gian thực.
Ứng dụng hoàn chỉnh này giới thiệu các tính năng nâng cao của SQL Connect, bao gồm:
- SQL gốc: Thực thi các câu lệnh Ngôn ngữ thao tác dữ liệu (DML) và Biểu thức bảng chung (CTE) phức tạp một cách an toàn bằng cách sử dụng
_executevà_select. - Khung hiển thị SQL: Tạo các đối tượng GraphQL nghiêm ngặt, an toàn về kiểu được hỗ trợ bởi các truy vấn Postgres động bằng cách sử dụng chỉ thị
@view. - Realtime Subscriptions (Đăng ký theo thời gian thực): Giúp giao diện người dùng trên giao diện người dùng luôn đồng bộ bằng cách sử dụng các trình kích hoạt
@refresh. - Giao dịch nguyên tử: Liên kết nhiều thao tác và xác thực trạng thái bằng cách sử dụng
@transactionvà@check. - (Không bắt buộc) Tìm kiếm không gian địa lý và tìm kiếm vectơ: Tận dụng PostGIS và pgvector để tìm các tài sản đang thịnh hành gần toạ độ của người dùng và thực hiện tìm kiếm ngữ nghĩa.
- (Không bắt buộc) Trình phân giải tuỳ chỉnh: Kết nối logic Cloud Run tuỳ chỉnh với giản đồ GraphQL để tạo tiêu đề giao dịch bằng AI.
Điều kiện tiên quyết
Bạn cần có kiến thức vững chắc về JavaScript/TypeScript, React và cú pháp cơ bản của SQL.
Kiến thức bạn sẽ học được
- Cách sử dụng SQL gốc để thu hẹp khoảng cách giữa GraphQL khai báo và logic PostgreSQL thô.
- Cách tích hợp các tiện ích Postgres như PostGIS trực tiếp vào các truy vấn cơ sở dữ liệu.
- Cách thực thi logic phức tạp bằng các khối
@transactionnguyên tử. - Cách tạo
@viewsan toàn về kiểu cho bảng xếp hạng và số liệu thống kê. - Cách thiết lập gói thuê bao theo thời gian thực bằng
@refresh.
Những thứ bạn cần
- Git
- Visual Studio Code
- Cài đặt Node.js
- Một dự án Firebase trong gói giá Blaze (trả tiền theo mức dùng) (bắt buộc đối với Trình phân giải tuỳ chỉnh và Vertex AI).
2. Thiết lập môi trường phát triển
Giai đoạn này hướng dẫn bạn thiết lập giao diện người dùng và định cấu hình phiên bản Cloud SQL cho các tính năng nâng cao.
- Sao chép kho lưu trữ dự án và cài đặt các phần phụ thuộc bắt buộc cho ứng dụng:
git clone https://github.com/firebaseextended/codelab-dataconnect-web cd codelab-dataconnect-web git switch emoji-init npm install
- Mở thư mục đã sao chép bằng Visual Studio Code rồi cài đặt Tiện ích Firebase SQL Connect cho Visual Studio.
- Trong thiết bị đầu cuối, hãy đảm bảo Firebase CLI của bạn hoàn toàn được cập nhật (điều này là cần thiết cho các tính năng mới như
@refreshvà SQL gốc):
npm uninstall -g firebase-tools npm install -g firebase-tools firebase login firebase use your-project-id firebase init
(Chọn Lưu trữ, Xác thực và Kết nối SQL).
Tạo SDK kết nối SQL: Chạy lệnh:
firebase dataconnect:sdk:generate
- Kết nối ứng dụng web của bạn với dự án Firebase: Đăng ký ứng dụng web của bạn trong dự án Firebase bằng bảng điều khiển Firebase:
- Mở dự án của bạn, rồi nhấp vào Thêm ứng dụng (chọn biểu tượng Web).
- Tạm thời bỏ qua chế độ thiết lập SDK và chế độ thiết lập cấu hình, nhưng nhớ sao chép đối tượng
firebaseConfigđã tạo. - Mở
lib/firebase.tsxtrong trình soạn thảo mã rồi thay thế phần giữ chỗ hiện có bằng cấu hình bạn vừa sao chép:
const firebaseConfig = {
apiKey: "API_KEY",
authDomain: "PROJECT_ID.firebaseapp.com",
projectId: "PROJECT_ID",
storageBucket: "PROJECT_ID.firebasestorage.app",
messagingSenderId: "SENDER_ID",
appId: "APP_ID"
};
- Chạy máy chủ phát triển:
npm run dev
3. Xem xét cơ sở mã khởi đầu
Trong phần này, bạn sẽ khám phá các khía cạnh chính của cơ sở mã khởi đầu của ứng dụng. Mặc dù bạn sẽ viết giản đồ và truy vấn từ đầu, nhưng bạn nên hiểu cách giao diện người dùng được kết nối để tương tác với SQL Connect.
Cấu trúc thư mục và tệp
Thư mục dataconnect/
Thư mục này chứa định nghĩa về phần phụ trợ của bạn – mọi thứ từ cấu trúc cơ sở dữ liệu đến các truy vấn SQL cụ thể mà ứng dụng của bạn được phép chạy.
schema/schema.gql: Nơi bạn sẽ xác định các bảng Postgres cơ sở bằng cách sử dụng các loại GraphQL tiêu chuẩn.schema/views.gql: Nơi bạn sẽ xác định các khung hiển thị SQL phức tạp, chỉ đọc (chẳng hạn như bảng xếp hạng) bằng cách sử dụng chỉ thị@view.friendly-exchange/queries.gqlvàmutations.gql: "Người kết nối" của bạn. Đây là nơi bạn sẽ xác định các truy vấn chính xác và SQL gốc (_execute,_select) mà ứng dụng của bạn cho phép.dataconnect.yaml: Tệp cấu hình quy định chế độ cài đặt triển khai Cloud SQL và tạo SDK.
Thư mục lib/
Chứa logic ứng dụng, hoạt động xác thực và tương tác với Firebase SQL Connect SDK.
firebase.tsx: Xử lý quá trình khởi chạy ứng dụng Firebase, Auth và phiên bản SQL Connect.ExchangeService.tsx: Đây là cầu nối giữa các thành phần React và cơ sở dữ liệu. Nó bao bọc các hàm SDK đã tạo (chẳng hạn nhưbuyStockhoặcsellStock) trong các hàm không đồng bộ tiêu chuẩn để xử lý việc bắt lỗi, logic nghiệp vụ và thông báo dạng thông báo.
SDK được tạo
Khi bạn viết một truy vấn hoặc đột biến trong SQL Connect, tiện ích VS Code sẽ tự động tạo một SDK có kiểu dữ liệu mạnh. Trong dự án này, giao diện người dùng nhập trực tiếp các hàm này từ @dataconnect/generated.
4. Xác định giản đồ cho hoạt động trao đổi biểu tượng cảm xúc
Trong phần này, bạn sẽ xác định cấu trúc và mối quan hệ giữa các thực thể chính trong ứng dụng giao dịch. Các thực thể như User, Emoji, StockOwnership, Event và PriceHistory được ánh xạ tới các bảng cơ sở dữ liệu, với các mối quan hệ được thiết lập bằng cách sử dụng Firebase SQL Connect và chỉ thị lược đồ GraphQL.
Sau khi bạn thiết lập lược đồ này, ứng dụng sẽ sẵn sàng xử lý mọi thứ, từ việc thực hiện giao dịch mua/bán và cập nhật bảng xếp hạng toàn cầu cho đến việc lập bản đồ xu hướng không gian địa lý tại địa phương.
Các thực thể và mối quan hệ cốt lõi
- Biểu tượng cảm xúc: Chứa các thông tin chính như biểu tượng, tên, giá và xu hướng mà ứng dụng dùng để hiển thị thị trường.
- Người dùng: Theo dõi hồ sơ của người giao dịch, số điểm hiện có (đơn vị tiền tệ) và toạ độ địa lý để quét radar cục bộ.
- Mối quan hệ: Bảng kết hợp
StockOwnershiptheo dõi chính xác số lượng lượt chia sẻ mà một người dùng cụ thể sở hữu đối với một biểu tượng cảm xúc cụ thể. Các loạiEventvàPriceHistoryđóng vai trò là sổ cái bất biến, ghi lại tác động của thị trường và các điểm giá trong quá khứ theo thời gian.
Thiết lập bảng Người dùng
Loại User xác định một người giao dịch trong hệ thống, theo dõi số dư, vai trò và vị trí thực tế của họ cho các truy vấn không gian địa lý.
Sao chép và dán đoạn mã sau vào tệp dataconnect/schema/schema.gql:
# Users
# user-stockOwnership is a one-to-many relationship, user-events is a one-to-many relationship
# Utilizes the Firebase Auth uid expression as the primary key
type User @table {
id: String! @default(expr: "auth.uid")
username: String!
profileImage: String
role: String! @default(value: "USER")
points: Float! @default(value: 100.0)
city: String @default(value: "Las Vegas")
latitude: Float @default(value: 36.1699)
longitude: Float @default(value: -115.1398)
}
Điểm chính cần ghi nhớ:
id: Liên kết trực tiếp với Xác thực Firebase bằng cách sử dụng@default(expr: "auth.uid"). Điều này đảm bảo danh tính cơ sở dữ liệu và danh tính Auth là 1:1 một cách an toàn, ngăn người dùng giả mạo mã nhận dạng.points: Đơn vị tiền tệ ảo được dùng để giao dịch, mặc định là100.0đối với người dùng mới.
Thiết lập bảng Emoji
Loại Emoji xác định tài sản chính đang được giao dịch, bao gồm cả các trường để tìm kiếm văn bản tiêu chuẩn.
Sao chép và dán đoạn mã này vào tệp dataconnect/schema/schema.gql:
# Emojis
# emoji-stockOwnership is a one-to-many relationship, emoji-priceHistory is a one-to-many relationship
# Implements @searchable directives for full-text search
type Emoji @table {
id: UUID! @default(expr: "uuidV4()")
symbol: String!
name: String! @searchable
tags: [String!]
description: String! @searchable
currentPrice: Float! @default(value: 10.0)
trend: Float! @default(value: 0.0)
}
Điểm chính cần ghi nhớ:
namevàdescription: Sử dụng chỉ thị@searchableđể tối ưu hoá các cột này cho tính năng tìm kiếm toàn văn tiêu chuẩn.
Thiết lập bảng StockOwnership
Loại StockOwnership là một bảng kết hợp xử lý mối quan hệ nhiều-nhiều giữa người dùng và biểu tượng cảm xúc mà họ sở hữu. Sao chép và dán đoạn mã này vào tệp dataconnect/schema/schema.gql:
# Join table for many-to-many relationship between users and emojis
# The 'key' param signifies the primary key(s) of this table
# In this case, the keys are [user, emoji], the generated fields of the reference types
type StockOwnership @table(key: ["user", "emoji"]) {
user: User!
emoji: Emoji!
shares: Int! @default(value: 0)
}
Điểm chính cần ghi nhớ:
key: ["user", "emoji"]: Tạo khoá chính tổng hợp. Một người dùng không thể có hai bản ghi riêng biệt cho cùng một biểu tượng cảm xúc; điều này đảm bảo tính duy nhất cho mỗi cặp.- Tham chiếu ngầm: Bằng cách tham chiếu trực tiếp các loại
UservàEmoji, SQL Connect sẽ tự động tạo các khoá ngoàiuserId: String!vàemojiId: UUID!ở chế độ nền.
Thiết lập bảng Event và PriceHistory
Các loại này đại diện cho sổ cái của ứng dụng, ghi nhật ký chính xác những gì đã xảy ra và cách giá thay đổi. Sao chép và dán các đoạn mã cuối cùng vào tệp dataconnect/schema/schema.gql:
# Events
# Event-User is a many-to-one relationship, Event-Emoji is a many-to-one relationship
# Evaluates the createdAt timestamp purely on the server side using the request.time expression
type Event @table {
id: UUID! @default(expr: "uuidV4()")
user: User!
emoji: Emoji!
impact: Float!
description: String!
createdAt: Timestamp! @default(expr: "request.time")
}
# Price History
# PriceHistory-Emoji is a many-to-one relationship
type PriceHistory @table {
id: UUID! @default(expr: "uuidV4()")
emoji: Emoji!
price: Float!
recordedAt: Timestamp! @default(expr: "request.time")
}
Điểm chính cần ghi nhớ:
createdAtvàrecordedAt: Tự động đặt thành thời gian chính xác khi giao dịch cơ sở dữ liệu diễn ra bằng cách sử dụng@default(expr: "request.time"). Điều này ngăn các ứng dụng sửa đổi dấu thời gian.
Các trường và giá trị mặc định được tạo tự động
Giản đồ này dựa vào các biểu thức như @default(expr: "uuidV4()") và @default(expr: "auth.uid") để tự động tạo mã nhận dạng duy nhất và thực thi quyền sở hữu mà không yêu cầu ứng dụng khách cung cấp các biểu thức đó.
5. Truy xuất dữ liệu về thị trường và dữ liệu người dùng
Trong phần này, bạn sẽ chèn dữ liệu thị trường mô phỏng vào cơ sở dữ liệu, sau đó triển khai các trình kết nối (truy vấn) và mã TypeScript để gọi các trình kết nối này trên ứng dụng web. Đến cuối cùng, ứng dụng của bạn sẽ có thể tìm nạp và hiển thị động thị trường biểu tượng cảm xúc trực tiếp, hồ sơ người dùng và bảng xếp hạng ngay từ cơ sở dữ liệu.
Chèn dữ liệu giả lập về thị trường và người dùng
- Trong VSCode, hãy mở
dataconnect/seed.gql. - Đảm bảo các trình mô phỏng trong tiện ích Firebase SQL Connect đang chạy (hoặc phiên bản Cloud SQL của bạn đã kết nối).
- Bạn sẽ thấy nút Run (local) (Chạy (cục bộ)) hoặc Run (Production) (Chạy (Sản xuất)) CodeLens ở đầu tệp. Nhấp vào đây để chèn dữ liệu biểu tượng cảm xúc mô phỏng và nhật ký giá ban đầu vào cơ sở dữ liệu của bạn.
- Kiểm tra thiết bị đầu cuối Thực thi kết nối SQL để xác nhận rằng dữ liệu đã được thêm thành công.
Triển khai các truy vấn cơ bản
Trước tiên, hãy truy vấn các bảng chuẩn mà bạn đã xác định trong giản đồ.
- Mở
dataconnect/friendly-exchange/queries.gql. - Thêm các truy vấn sau để truy xuất dữ liệu trên trang tổng quan, hồ sơ người dùng và nhật ký giá cơ bản:
# Get dashboard data including top emojis by price and recent market events
query GetDashboardData
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
emojis(orderBy: [{ currentPrice: DESC }]) {
id
symbol
name
description
currentPrice
trend
}
events(orderBy: [{ createdAt: DESC }], limit: 15) {
id
description
impact
createdAt
user {
username
profileImage
}
emoji {
symbol
}
}
}
# Get current authenticated user profile and their stock ownership using auth.uid
query GetUserProfile @auth(level: USER) {
user(id_expr: "auth.uid") {
points
username
profileImage
role
stockOwnerships_on_user {
shares
emoji {
id
symbol
currentPrice
name
}
}
city
latitude
longitude
}
}
# Get price history for a specific emoji ordered by time
query GetPriceHistory($emojiId: UUID!, $limit: Int)
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
priceHistories(
where: { emojiId: { eq: $emojiId } }
orderBy: [{ recordedAt: ASC }]
limit: $limit
) {
price
recordedAt
}
}
Điểm chính cần ghi nhớ:
emojis()/events(): Các trường truy vấn GraphQL được tạo tự động để tìm nạp dữ liệu trực tiếp từ các bảng của bạn.id_expr: "auth.uid": Bảo mật quyền truy cập bằng cách tìm nạp hồ sơ người dùng khớp với mã thông báo của người dùng Firebase hiện tại đã xác thực._on_: Cho phép truy cập trực tiếp vào các trường từ một loại được liên kết có mối quan hệ khoá ngoài.stockOwnerships_on_usertìm nạp toàn bộ danh mục đầu tư của người dùng trong một truy vấn.insecureReason: Bắt buộc khi hiển thị các thao tác choPUBLIC. Nó ghi lại rõ ràng lý do khiến dữ liệu này an toàn khi hiển thị mà không cần xác thực.
Tạo các khung hiển thị SQL an toàn về kiểu
Trước khi viết SQL tuỳ chỉnh, bạn cần hiểu rõ các cách mà Firebase SQL Connect xử lý truy vấn:
- GraphQL tiêu chuẩn: Phù hợp nhất cho các mối quan hệ CRUD cơ bản và đơn giản với độ an toàn về kiểu nghiêm ngặt từ đầu đến cuối.
- Khung hiển thị SQL (
@view): Phù hợp nhất với SQL chỉ đọc, phức tạp (chẳng hạn như bảng xếp hạng sử dụng các hàm cửa sổ) khi bạn vẫn muốn một đối tượng GraphQL nghiêm ngặt, an toàn về kiểu được trả về cho máy khách. - SQL gốc (
_execute/_select): Phù hợp nhất để thực thi DML, CTE hoặc các tiện ích PostGIS trực tiếp. Bạn đánh đổi việc nhập nghiêm ngặt tại thời gian biên dịch để có được tính linh hoạt tối đa tại thời gian thực thi (trả về JSON động).
Để tạo bảng xếp hạng và biểu đồ đường, chúng ta cần tính toán đường trung bình động và xếp hạng người dùng. Đây là một trường hợp sử dụng cho @view.
- Mở
dataconnect/schema/views.gql. - Thêm các khung hiển thị sau để tính toán số liệu thống kê cần thiết trên máy chủ:
# Rank users on a leaderboard based on their total net worth
type TopTrader
@view(
sql: """
SELECT
u.id,
u.username,
u.profile_image,
(u.points + COALESCE(SUM(so.shares * e.current_price), 0)) AS net_worth,
RANK() OVER (ORDER BY (u.points + COALESCE(SUM(so.shares * e.current_price), 0)) DESC) AS rank
FROM "user" u
LEFT JOIN stock_ownership so ON u.id = so.user_id
LEFT JOIN emoji e ON so.emoji_id = e.id
WHERE u.id != 'system_market_maker'
GROUP BY u.id, u.username, u.profile_image, u.points
"""
) {
id: String
username: String
profileImage: String
netWorth: Float
rank: Int
}
# Identify the top shareholder (whale) for each emoji and their total ownership percentage
type EmojiWhaleStat
@view(
sql: """
WITH total_shares AS (
SELECT emoji_id, SUM(shares) AS total_supply
FROM stock_ownership WHERE shares > 0 GROUP BY emoji_id
),
ranked_holders AS (
SELECT
so.emoji_id, u.username AS whale_username, u.profile_image AS whale_profile_image,
so.shares AS whale_shares, ts.total_supply,
ROUND((so.shares::DECIMAL / NULLIF(ts.total_supply, 0)) * 100, 2) AS whale_percentage,
RANK() OVER (PARTITION BY so.emoji_id ORDER BY so.shares DESC) AS holder_rank
FROM stock_ownership so
JOIN "user" u ON u.id = so.user_id
JOIN total_shares ts ON ts.emoji_id = so.emoji_id
WHERE so.shares > 0
)
SELECT emoji_id, whale_username, whale_profile_image, whale_shares, total_supply, whale_percentage
FROM ranked_holders WHERE holder_rank = 1
"""
) {
emojiId: UUID
whaleUsername: String
whaleProfileImage: String
whaleShares: Int
totalSupply: Int
whalePercentage: Float
}
# Calculate the moving average of historical prices for each emoji
type EmojiHistoryStat
@view(
sql: """
SELECT
emoji_id, price, recorded_at,
AVG(price) OVER (PARTITION BY emoji_id ORDER BY recorded_at ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) as moving_average
FROM price_history
"""
) {
emojiId: UUID
price: Float
recordedAt: Timestamp
movingAverage: Float
}
# Combine recent price updates and major news events into a single chronological feed
type TickerFeed
@view(
sql: """
WITH latest_prices AS (
SELECT emoji_id, MAX(recorded_at) as last_trade_time
FROM price_history GROUP BY emoji_id
)
SELECT
'PRICE' as type, e.symbol, e.name, e.current_price, e.trend,
'' as description, lp.last_trade_time as event_time
FROM emoji e JOIN latest_prices lp ON e.id = lp.emoji_id
UNION ALL
SELECT
'NEWS' as type, e.symbol, '' as name, 0 as current_price, 0 as trend,
ev.description, ev.created_at as event_time
FROM event ev JOIN emoji e ON ev.emoji_id = e.id
"""
) {
type: String
symbol: String
name: String
currentPrice: Float
trend: Float
description: String
eventTime: Timestamp
}
# Retrieve the 15 most recent price points for each emoji to render sparkline charts
type EmojiSparkline
@view(
sql: """
WITH RankedPrices AS (
SELECT
emoji_id, price, recorded_at,
ROW_NUMBER() OVER(PARTITION BY emoji_id ORDER BY recorded_at DESC) as rn
FROM price_history
)
SELECT emoji_id, price, recorded_at
FROM RankedPrices WHERE rn <= 15 ORDER BY recorded_at ASC
"""
) {
emojiId: UUID
price: Float
recordedAt: Timestamp
}
Bây giờ, hãy mở dataconnect/friendly-exchange/queries.gql và thay thế các TODO để tìm nạp dữ liệu từ các khung hiển thị mới:
# Get emoji whale statistics to identify top shareholders from emojiWhaleStats view
query GetEmojiWhaleStats
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
emojiWhaleStats {
emojiId
whaleUsername
whaleProfileImage
whaleShares
totalSupply
whalePercentage
}
}
# Get historical price and moving average stats for a specific emoji from emojiHistoryStats view
query GetEmojiHistoryStats($emojiId: UUID!)
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
emojiHistoryStats(
where: { emojiId: { eq: $emojiId } }
orderBy: [{ recordedAt: ASC }]
limit: 50
) {
price
movingAverage
recordedAt
}
}
# List top traders ordered by rank from topTraders view
query GetTopTraders
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
topTraders(orderBy: [{ rank: ASC }]) {
id
username
profileImage
netWorth
rank
}
}
# Get chronological market ticker feed of recent events from tickerFeeds view
query GetChronologicalTicker
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
tickerFeeds(orderBy: [{ eventTime: DESC }], limit: 30) {
type
symbol
name
currentPrice
trend
description
eventTime
}
}
# Get simple price points for rendering emoji sparkline charts from emojiSparklines view
query GetEmojiSparklines
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
emojiSparklines {
emojiId
price
recordedAt
}
}
Điểm chính cần ghi nhớ
@view: Đóng gói logic cơ sở dữ liệu phức tạp trên máy chủ trong khi vẫn giữ cho mã phía máy khách được nhập một cách nghiêm ngặt. SQL Connect liên kết các trường GraphQL trên loại@viewvới các cột do câu lệnhSELECTtrả về.- Chỉ đọc: Các khung hiển thị không có khoá chính và không thể thay đổi trực tiếp.
- Tạo truy vấn: Lưu ý cách
topTraders()vàemojiSparklines()hoạt động giống hệt như khi truy vấn một bảng tiêu chuẩn.
Triển khai cụm từ tìm kiếm
SQL Connect tự động tạo các cụm từ tìm kiếm tiêu chuẩn cho mọi trường được đánh dấu bằng chỉ thị @searchable trong giản đồ của bạn.
Thêm truy vấn sau vào dataconnect/friendly-exchange/queries.gql để bật tính năng tìm kiếm toàn văn:
# Search emojis using full-text search query
query SearchEmojis($query: String)
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
emojis_search(query: $query) {
id
symbol
name
description
currentPrice
trend
}
}
Điểm chính cần ghi nhớ
emojis_search: Trường truy vấn được tạo tự động vì bạn đã áp dụng@searchablecho các trườngnamevàdescriptiontrong giản đồEmoji.
Tạo SDK
Vì bạn đã xác định các truy vấn và khung hiển thị mới trong tệp GraphQL, nên bạn phải chạy trình tạo SDK để giao diện người dùng TypeScript có thể sử dụng các truy vấn và khung hiển thị đó một cách an toàn.
Mở cửa sổ dòng lệnh rồi chạy:
firebase dataconnect:sdk:generate
Tích hợp các truy vấn trong ứng dụng web
Trình biên dịch Firebase SQL Connect tạo ra các SDK dựa trên tệp .gql của bạn. Vì đây là ứng dụng theo thời gian thực, nên bạn sẽ sử dụng phương thức subscribe cùng với các tham chiếu truy vấn đã tạo trên nhiều thành phần.
Thay thế các khối useEffect trống trong các tệp sau bằng logic bên dưới:
1. Trang chủ (
app/page.tsx
)
import { subscribe } from "@firebase/data-connect";
import {
getDashboardDataRef,
searchEmojisRef,
getChronologicalTickerRef,
getUserProfileRef,
} from "@dataconnect/generated";
// Inside the Home component:
useEffect(() => {
// Subscribe to realtime updates for the main market dashboard data including top emojis and recent events
const unsubscribe = subscribe(
getDashboardDataRef(),
(res) => {
if (res.data) setDashboardData(res.data);
setIsDashboardLoading(false);
},
(err) => {
console.error("Dashboard Realtime Error:", err);
setIsDashboardLoading(false);
},
);
return () => unsubscribe();
}, [user]);
useEffect(() => {
// Subscribe to a realtime chronological ticker feed combining recent price updates and major news events
const unsubscribe = subscribe(
getChronologicalTickerRef(),
(res) => {
if (res.data) setTickerData(res.data);
},
(err) => console.error("Ticker Realtime Error:", err),
);
return () => unsubscribe();
}, []);
useEffect(() => {
if (loading || !user) return;
// Subscribe to realtime updates for the authenticated user's profile and stock ownership
const unsubscribe = subscribe(
getUserProfileRef(),
(res) => {
if (res.data) setProfileData(res.data);
},
(err) => console.error("Profile Error:", err),
);
return () => unsubscribe();
}, [user, loading]);
useEffect(() => {
if (!debouncedSearch) {
setSearchData(null);
return;
}
// Subscribe to realtime full-text search results for emojis based on user input
const unsubscribe = subscribe(
searchEmojisRef({ query: debouncedSearch }),
(res) => {
if (res.data) setSearchData(res.data.emojis_search);
setIsSearchLoading(false);
},
(err) => {
console.error("Text Search Error:", err);
setIsSearchLoading(false);
},
);
return () => unsubscribe();
}, [debouncedSearch]);
2. Các thành phần trong hồ sơ người dùng
app/profile/page.tsx
, update the hooks:
import { subscribe } from "@firebase/data-connect";
import { getUserProfileRef } from "@dataconnect/generated";
useEffect(() => {
// Subscribe to realtime updates for the authenticated user's profile and stock ownership
const unsubscribe = subscribe(
getUserProfileRef(),
(res) => {
if (res.data) {
setData(res.data);
}
setIsLoading(false);
},
(err) => {
console.error("Profile Realtime Error:", err);
setIsLoading(false);
},
);
return () => unsubscribe();
}, []);
components/NavBar.tsx
:
useEffect(() => {
// Subscribe to realtime updates for the authenticated user's profile and stock ownership
const unsub = subscribe(
getUserProfileRef(),
(res) => {
if (res.data) setData(res.data);
},
(err) => console.error("Navbar Balance Realtime Error:", err),
);
return () => unsub();
}, []);
Đối với components/FloatingMenu.tsx, hãy thay thế đối tượng const { data } theo cách thủ công bằng hook được tạo:
const { data, refetch: refetchDashboard } = useGetDashboardData();
useEffect(() => {
if (!user) return;
// Subscribe to realtime updates for the authenticated user's profile
const unsub = subscribe(getUserProfileRef(), (res) => {
if (res.data) {
setProfileData(res.data);
setOptimisticRole(null);
}
});
return () => unsub();
}, [user]);
Điểm chính cần ghi nhớ
getUserProfileRef/getDashboardDataRef: Các hàm được tạo tự động giúp chuẩn bị các truy vấn GraphQL để thực thi, duy trì các kiểu dữ liệu nghiêm ngặt do bảng và khung hiển thị của bạn xác định.subscribe: Một phương thức SQL Connect SDK lắng nghe truy vấn. Hiện tại, hàm này chỉ cần tìm nạp dữ liệu khi thành phần được gắn kết, nhưng ở bước sau, chúng ta sẽ nâng cấp phần phụ trợ để tự động kích hoạt hàm này bất cứ khi nào cơ sở dữ liệu thay đổi!
- Market Panel (
components/MarketPanel.tsx): Tương tự, trong thành phần MarketPanel (components/MarketPanel.tsx), bạn có thể thay thếTODOđể gọi nhiều truy vấn cùng lúc nhằm tạo thanh bên.
import { subscribe } from "@firebase/data-connect";
import { getDashboardDataRef, getEmojiSparklinesRef } from "@dataconnect/generated";
// Inside the MarketPanel component:
useEffect(() => {
// Subscribe to realtime updates for the main market dashboard data including top emojis and recent events
const unsub = subscribe(
getDashboardDataRef(),
(res) => {
if (res.data) setData(res.data);
},
(err) => console.error("Market Panel Realtime Error:", err)
);
return () => unsub();
}, []);
useEffect(() => {
// Subscribe to realtime price history updates to render emoji sparkline charts
const unsub = subscribe(
getEmojiSparklinesRef(),
(res) => {
if (res.data?.emojiSparklines) {
setSparklineRawData(res.data.emojiSparklines);
}
},
(err) => console.error("Global Sparklines Error:", err)
);
return () => unsub();
}, []);
- Trang bảng xếp hạng (
app/leaderboard/page.tsx)
import { subscribe } from "@firebase/data-connect";
import { getTopTradersRef } from "@dataconnect/generated";
// Inside the Leaderboard component:
useEffect(() => {
// Subscribe to realtime updates for the global leaderboard ranking top traders by net worth
const unsubscribe = subscribe(
getTopTradersRef(),
(res) => {
if (res.data) setData(res.data);
setIsLoading(false);
},
(err) => {
console.error("Leaderboard Realtime Error:", err);
setIsLoading(false);
},
);
return () => unsubscribe();
}, []);
- Cửa sổ phương thức biểu tượng cảm xúc (
components/EmojiModal.tsx)
import { subscribe } from "@firebase/data-connect";
import {
getEmojiHistoryStatsRef,
getEmojiWhaleStatsRef,
} from "@dataconnect/generated";
// Inside the EmojiModal component:
useEffect(() => {
if (!emoji?.id) return;
setStatsLoading(true);
// Subscribe to realtime historical price and moving average statistics for the selected emoji
const unsub = subscribe(
getEmojiHistoryStatsRef({ emojiId: emoji.id }),
(res) => {
if (res.data) setStatsData(res.data);
setStatsLoading(false);
},
(err) => {
console.error("History Realtime Error:", err);
setStatsLoading(false);
},
);
return () => unsub();
}, [emoji?.id]);
useEffect(() => {
// Subscribe to realtime whale statistics to identify the top shareholder for the selected emoji
const unsub = subscribe(
getEmojiWhaleStatsRef(),
(res) => {
if (res.data) setWhaleData(res.data);
},
(err) => console.error("Whale Realtime Error:", err),
);
return () => unsub();
}, []);
Ví dụ thực tế
Tải lại ứng dụng web để xem các truy vấn đang hoạt động. Trang chủ và thanh bên hiện hiển thị danh sách biểu tượng cảm xúc, tìm nạp dữ liệu trực tiếp từ cơ sở dữ liệu PostgreSQL.
6. Xử lý thông tin cập nhật của người dùng và giao dịch trên thị trường
Trong phần này, bạn sẽ triển khai chức năng đăng nhập của người dùng bằng cách sử dụng Xác thực Firebase để chèn hoặc cập nhật hồ sơ người dùng (chẳng hạn như tên hiển thị và vị trí thực tế của họ) trong Firebase SQL Connect. Bạn cũng sẽ sử dụng các chỉ thị @transaction và @check của SQL Connect để thực thi an toàn một sự kiện thị trường nhiều bước, có tính nguyên tử.
Triển khai các trình kết nối người dùng và vị trí
Mở dataconnect/friendly-exchange/mutations.gql. Thay thế TODO bằng cách thêm các đột biến sau để xử lý việc tạo, cập nhật và định vị người dùng:
# Upserts a user record using the Firebase Auth uid expression as the primary key
# Upsert (update or insert) a user's profile information
mutation UpsertUser($username: String!, $profileImage: String!)
@auth(level: USER) {
user_upsert(
data: {
id_expr: "auth.uid"
username: $username
profileImage: $profileImage
}
)
}
# Update a user's role
mutation UpdateUserRole($role: String!) @auth(level: USER) {
user_update(key: { id_expr: "auth.uid" }, data: { role: $role })
}
# Update a user's location
mutation UpdateUserLocation(
$city: String!
$latitude: Float!
$longitude: Float!
) @auth(level: USER) {
user_update(
key: { id_expr: "auth.uid" }
data: { city: $city, latitude: $latitude, longitude: $longitude }
)
}
# Trigger a new market event for an emoji
mutation TriggerEvent(
$emojiId: UUID!
$impact: Float!
$description: String!
$now: Timestamp!
) @auth(level: USER) {
event_insert(
data: {
userId_expr: "auth.uid"
emojiId: $emojiId
impact: $impact
description: $description
createdAt: $now
}
)
}
Điểm chính cần ghi nhớ
id_expr: "auth.uid": Phương thức này sử dụngauth.uid, được cung cấp trực tiếp bằng mã thông báo Xác thực Firebase. Bằng cách đánh giá phía máy chủ này, bạn đảm bảo rằng người dùng chỉ có thể cập nhật dữ liệu hồ sơ của chính họ, thêm một lớp bảo mật không thể phá vỡ.
Logic chuỗi với @transaction
Tiếp theo, bạn sẽ triển khai "Nhà tạo lập thị trường" mà quản trị viên có thể kích hoạt để mô phỏng hoạt động ngẫu nhiên trên thị trường. Vì thao tác này yêu cầu cập nhật giá của biểu tượng cảm xúc, ghi nhật ký sự kiện và cập nhật quyền sở hữu cổ phiếu của hệ thống cùng một lúc, nên chúng ta cần một giao dịch nguyên tử.
Thêm đột biến này vào tệp mutations.gql:
# Execute a market maker trade to adjust emoji price and shares
mutation MarketMakerTrade(
$emojiId: UUID!
$priceImpact: Float!
$shareDelta: Int!
$eventDesc: String!
$newPrice: Float!
)
@auth(
level: USER
insecureReason: "This operation is safe to expose to any user."
)
@transaction {
query @redact {
user(key: { id_expr: "auth.uid" })
@check(
expr: "this != null && this.role == 'ADMIN'",
message: "Access Denied: You must have the ADMIN role to deploy the Market Maker bot."
) {
role
}
}
stockOwnership_upsert(
data: {
userId: "system_market_maker"
emojiId: $emojiId
shares_update: { inc: $shareDelta }
}
)
emoji_update(
id: $emojiId
data: { currentPrice_update: { inc: $priceImpact }, trend: $priceImpact }
)
event_insert(
data: {
userId: "system_market_maker"
emojiId: $emojiId
impact: $priceImpact
description: $eventDesc
}
)
priceHistory_insert(data: { emojiId: $emojiId, price: $newPrice })
}
Điểm chính cần ghi nhớ
@transaction: Đảm bảo tất cả các thao tác trên cơ sở dữ liệu (upserting stock, updating emoji price, logging the event) đều thành công hoặc thất bại cùng nhau.@check: Một chỉ thị đánh giá một điều kiện trước khi tiếp tục. Ở đây, hệ thống sẽ kiểm tra xemrolecủa người dùng đã xác thực có phải là'ADMIN'hay không. Nếu người dùng chỉ là một'USER'tiêu chuẩn, thì toàn bộ giao dịch sẽ bị từ chối và quay trở lại.@redact: Ngăn kết quả truy vấn (chẳng hạn như quy trình kiểm tra vai trò của người dùng) được trả về cho máy khách trong tải trọng phản hồi, giúp phản hồi giao dịch không bị lẫn lộn.
Tạo SDK
Vì bạn đã xác định các đột biến mới trong tệp GraphQL, nên bạn phải chạy trình tạo SDK để giao diện người dùng TypeScript có thể gọi đột biến đó.
Mở cửa sổ dòng lệnh rồi chạy:
firebase dataconnect:sdk:generate
Tích hợp các đột biến trong ứng dụng web
Trong ứng dụng web, bạn sẽ bao bọc các đột biến SDK đã tạo này trong các hàm không đồng bộ tiêu chuẩn để xử lý việc phát hiện lỗi và thông báo trên giao diện người dùng.
Mở lib/ExchangeService.tsx rồi xem xét các hàm bao bọc. Thay thế các khối TODO bằng các cách triển khai sau:
import {
upsertUser,
updateUserLocation,
marketMakerTrade,
updateUserRole,
triggerMarketCrash,
} from "@dataconnect/generated";
// Upsert (update or insert) a user's profile information and log the event
export const executeUpsertUser = async (
username: string,
profileImage: string,
logEvent: (key: LogEventKey, params?: any) => void,
): Promise<void> => {
logEvent("UPSERT_USER_MUTATION", { username });
await upsertUser({ username, profileImage });
};
// Update a user's role and log the event
export const executeUpdateRole = async (
role: string,
logEvent: (key: LogEventKey, params?: any) => void
): Promise<void> => {
logEvent("UPDATE_USER_ROLE_MUTATION", { role });
await updateUserRole({ role });
};
// Update a user's city and geographic coordinates
export const executeUpdateLocation = async (
city: string,
latitude: number,
longitude: number,
): Promise<void> => {
await updateUserLocation({ city, latitude, longitude });
};
// Execute a random market maker trade and adjust an emoji's stock price
export const executeManualBotTrade = async (
randomEmoji: any,
username: string,
logEvent: (key: LogEventKey, params?: any) => void,
): Promise<{ isBuy: boolean; tradeAmount: number }> => {
logEvent("MARKET_MAKER_TRADE");
const isBuy = Math.random() > 0.5;
const tradeAmount = Number((Math.random() * (10 - 2) + 2).toFixed(2));
await marketMakerTrade({
emojiId: randomEmoji.id,
priceImpact: isBuy ? tradeAmount : -tradeAmount,
shareDelta: isBuy ? 10 : -10,
eventDesc: `Admin ${username} triggered market event: ${randomEmoji.symbol} went ${isBuy ? "up" : "down"} by $${tradeAmount.toFixed(2)}.`,
newPrice: Math.max(0.01, randomEmoji.currentPrice + (isBuy ? tradeAmount : -tradeAmount)),
});
return { isBuy, tradeAmount };
};
Triggering upsert on login: In app/src/components/Navbar.tsx, you can see how executeUpsertUser is called immediately after Firebase Authentication successfully signs a user in via Google Popup. This guarantees the SQL Connect database is synced with Firebase Auth.
See it in action
Now, click the Sign In button in the navbar. You can sign in using Firebase Authentication. After signing in:
- Navigate to your Profile and test out the Auto-Locate button. When you click Update Coordinates, the
UpdateUserLocationmutation will execute. - Open the Floating Control Panel (the purple icon in the bottom right corner).
- Click USER and switch your authorization level to ADMIN.
- Click Trigger random market activity. Because your role is now
'ADMIN', the@checkdirective passes, the@transactionexecutes, and you will instantly see the market prices update across your application!
7. Advanced operations with Native SQL
In this section, you will use Native SQL to execute complex Data Manipulation Language (DML) statements and leverage PostgreSQL-specific extensions.
While standard GraphQL and @views are ideal for strictly-typed CRUD and read-only operations, Native SQL provides execution-time flexibility. It allows you to use Common Table Expressions (CTEs) to chain multiple updates in a single database round-trip, and lets you query native PostgreSQL extensions directly.
Enable the PostGIS extension
Before we write geospatial queries, you need to enable the PostGIS extension on your Cloud SQL database.
- Navigate to the Google Cloud Console.
- Go to Cloud SQL -> select your provisioned instance -> click Cloud SQL Studio.
- Log into your database and execute the following command:
CREATE EXTENSION IF NOT EXISTS postgis;
Implement Native SQL Queries
Let's use Native SQL to find trending emojis near the user's physical location, and to calculate the top emojis per city using complex ranking.
- Open
dataconnect/friendly-exchange/queries.gql. - Add the following Native SQL queries using the
_selectfield:
# Get top trending emojis partitioned by user city using native SQL
query GetTopEmojisByCity
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
cityTrends: _select(
sql: """
WITH city_shares AS (
SELECT
u.city,
AVG(u.latitude) as latitude,
AVG(u.longitude) as longitude,
e.id as emoji_id,
e.symbol,
e.name,
SUM(so.shares) as total_shares,
RANK() OVER (PARTITION BY u.city ORDER BY SUM(so.shares) DESC) as rank
FROM stock_ownership so
JOIN "user" u ON so.user_id = u.id
JOIN emoji e ON so.emoji_id = e.id
WHERE u.city IS NOT NULL AND u.latitude IS NOT NULL AND so.shares > 0
GROUP BY u.city, e.id, e.symbol, e.name
)
SELECT city, latitude, longitude, emoji_id, symbol, name, total_shares
FROM city_shares
WHERE rank = 1
ORDER BY city ASC
"""
params: []
)
}
# Get trending emojis within a geographic radius using native SQL and PostGIS extension
query GetTrendingEmojisNearMe(
$userLng: Float!
$userLat: Float!
$radiusMeters: Float!
)
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
) {
regionalTrends: _select(
sql: """
SELECT
e.id,
e.symbol,
e.name,
e.current_price,
e.trend,
COUNT(so.shares) AS regional_holders,
SUM(so.shares) AS regional_shares
FROM emoji e
JOIN stock_ownership so ON so.emoji_id = e.id
JOIN "user" u ON u.id = so.user_id
WHERE u.latitude IS NOT NULL
AND u.longitude IS NOT NULL
AND so.shares > 0
AND ST_DWithin(
ST_MakePoint(u.longitude, u.latitude)::geography,
ST_MakePoint($1, $2)::geography,
$3
)
GROUP BY e.id, e.symbol, e.name, e.current_price, e.trend
ORDER BY regional_shares DESC
LIMIT 10
"""
params: [$userLng, $userLat, $radiusMeters]
)
}
Key Takeaways
_select: Executes a Data Query Language (DQL) statement returning a JSON array ([Any]).ST_DWithin: A native PostGIS function that calculates distances on a sphere. Native SQL allows you to use this without mapping complex geometry types into your GraphQL schema.params: Variables like$userLngare bound to the SQL string via positional parameters ($1,$2,$3), preventing SQL injection.
Implement Native SQL Mutations
When a user buys or sells a stock, the system must validate their funds, deduct the cost, add the shares, update the global emoji price, and log the history. Doing this across multiple standard mutations could lead to race conditions. Instead, we can use a CTE (WITH) to do this atomically in one Native SQL execution.
Open dataconnect/friendly-exchange/mutations.gql and replace the TODOs with the following Native SQL mutations:
# Buy shares of an emoji stock
mutation BuyStock($emojiId: UUID!, $amount: Int!, $isDiscounted: Boolean!)
@auth(level: USER) {
buyStock: _execute(
sql: """
WITH validated_params AS (
SELECT
$1::uuid AS emoji_id,
$2::int AS amount,
$3::boolean AS is_discounted,
$4::text AS user_id
),
target_emoji AS (
SELECT
e.id,
(e.current_price * (CASE WHEN vp.is_discounted THEN 0.5 ELSE 1.0 END) * vp.amount) AS total_cost
FROM emoji e
CROSS JOIN validated_params vp
WHERE e.id = vp.emoji_id
AND vp.amount > 0
AND vp.amount <= 100
),
deduct_funds AS (
UPDATE "user" u
SET points = u.points - te.total_cost
FROM target_emoji te, validated_params vp
WHERE u.id = vp.user_id AND u.points >= te.total_cost
RETURNING u.id
),
upsert_ownership AS (
INSERT INTO stock_ownership (user_id, emoji_id, shares)
SELECT vp.user_id, vp.emoji_id, vp.amount
FROM validated_params vp
WHERE EXISTS (SELECT 1 FROM deduct_funds)
ON CONFLICT (user_id, emoji_id) DO UPDATE
SET shares = stock_ownership.shares + EXCLUDED.shares
RETURNING stock_ownership.emoji_id
),
update_emoji AS (
UPDATE emoji e
SET
current_price = GREATEST(0.01, e.current_price + (e.current_price * 0.01 * vp.amount)),
trend = GREATEST(0.01, e.current_price + (e.current_price * 0.01 * vp.amount)) - e.current_price
FROM validated_params vp
WHERE e.id = vp.emoji_id AND EXISTS (SELECT 1 FROM deduct_funds)
RETURNING e.id, e.current_price, e.trend
)
INSERT INTO price_history (id, emoji_id, price, recorded_at)
SELECT gen_random_uuid(), ue.id, ue.current_price, NOW()
FROM update_emoji ue;
"""
params: [$emojiId, $amount, $isDiscounted, { _expr: "auth.uid" }]
)
}
# Sell shares of an emoji stock
mutation SellStock($emojiId: UUID!, $amount: Int!) @auth(level: USER) {
sellStock: _execute(
sql: """
WITH validated_params AS (
SELECT
$1::uuid AS emoji_id,
$2::int AS amount,
$3::text AS user_id
),
target_emoji AS (
SELECT
e.id,
(e.current_price * vp.amount) AS total_revenue,
GREATEST(0.01, e.current_price * POWER(0.99, vp.amount)) AS new_price
FROM emoji e
CROSS JOIN validated_params vp
WHERE e.id = vp.emoji_id
AND vp.amount > 0
AND vp.amount <= 100
),
check_shares AS (
SELECT so.user_id
FROM stock_ownership so
CROSS JOIN validated_params vp
WHERE so.user_id = vp.user_id
AND so.emoji_id = vp.emoji_id
AND so.shares >= vp.amount
),
add_funds AS (
UPDATE "user" u
SET points = u.points + te.total_revenue
FROM target_emoji te, validated_params vp
WHERE u.id = vp.user_id AND EXISTS (SELECT 1 FROM check_shares)
RETURNING u.id
),
update_ownership AS (
UPDATE stock_ownership so
SET shares = so.shares - vp.amount
FROM validated_params vp
WHERE so.user_id = vp.user_id
AND so.emoji_id = vp.emoji_id
AND EXISTS (SELECT 1 FROM check_shares)
AND EXISTS (SELECT 1 FROM add_funds)
),
update_emoji AS (
UPDATE emoji e
SET
current_price = te.new_price,
trend = te.new_price - e.current_price
FROM target_emoji te, validated_params vp
WHERE e.id = vp.emoji_id
AND EXISTS (SELECT 1 FROM check_shares)
AND EXISTS (SELECT 1 FROM add_funds)
RETURNING e.id, e.current_price, e.trend
)
INSERT INTO price_history (id, emoji_id, price, recorded_at)
SELECT gen_random_uuid(), ue.id, ue.current_price, NOW()
FROM update_emoji ue;
"""
params: [$emojiId, $amount, { _expr: "auth.uid" }]
)
}
Key Takeaways
_execute: Executes a Data Manipulation Language (DML) statement, such asUPDATE,INSERT, orDELETE.- Common Table Expressions (
WITH): Each block in the CTE depends on the previous one. For example,add_fundswill only execute ifcheck_sharesreturns a result. This handles the complex conditions completely within Postgres. - Context Injection:
{ _expr: "auth.uid" }injects the authenticated user's ID into the query directly on the server, enforcing security.
Generate the SDK
Because you have defined new queries and mutations in your GraphQL files, you must run the SDK generator so your TypeScript frontend can call it.
Open your terminal and run:
firebase dataconnect:sdk:generate
Integrate Native SQL in the web app
- Native SQL returns a flexible JSON payload rather than a strictly typed object. Because of this, it's essential to manually validate the returned data shape in your client code to handle the dynamic response.
- Execute Trades: In
lib/ExchangeService.tsx, we wrap the generatedbuyStockandsellStockSDKs. Notice how the return typesbuyResultandsellResultmust be manually validated as arrays, because_executereturns dynamic JSON data based on your specificRETURNINGclauses in the SQL strings. - Replace the empty
executeBuyStockandexecuteSellStockfunctions with your original complete code:
import { buyStock, sellStock, generateTradeHeadline, triggerEvent } from "@dataconnect/generated";
import { LogEventKey } from "./InspectorContext";
// Execute a stock purchase, validating limits and potentially generating an AI news headline for large trades
export const executeBuyStock = async (
emoji: any,
amount: number,
isDiscounted: boolean,
user: any,
logEvent: (key: LogEventKey, params?: any) => void,
): Promise<void> => {
const MAX_AMOUNT = 100;
if (!Number.isInteger(amount) || amount <= 0 || amount > MAX_AMOUNT) {
throw new Error(`Amount must be an integer between 1 and ${MAX_AMOUNT}.`);
}
const singleSharePrice = isDiscounted
? emoji.currentPrice * 0.5
: emoji.currentPrice;
const estimatedCost = singleSharePrice * amount;
const estimatedImpact = emoji.currentPrice * 0.05 * amount;
logEvent("BUY_STOCK_TRANSACTION", { amount, symbol: emoji.symbol });
const response = await buyStock({
emojiId: emoji.id,
amount: amount,
isDiscounted: isDiscounted,
});
const buyResult = response.data?.buyStock as any;
if (
!buyResult ||
buyResult === 0 ||
(Array.isArray(buyResult) && buyResult.length === 0)
) {
throw new Error(
"Transaction denied: Insufficient funds or price mismatch.",
);
}
const actualCost = Array.isArray(buyResult)
? buyResult[0].actual_cost
: estimatedCost;
const actualImpact = Array.isArray(buyResult)
? buyResult[0].actual_impact
: estimatedImpact;
// TODO: Optionally add a custom resolver to call AI to generate headline for this purchase
};
// Execute a stock sale, validating ownership and potentially generating an AI news headline for large trades
export const executeSellStock = async (
emoji: any,
amount: number,
ownedShares: number,
user: any,
logEvent: (key: LogEventKey, params?: any) => void,
): Promise<void> => {
const MAX_AMOUNT = 100;
if (!Number.isInteger(amount) || amount <= 0 || amount > MAX_AMOUNT) {
throw new Error(`Amount must be an integer between 1 and ${MAX_AMOUNT}.`);
}
if (amount > ownedShares) {
throw new Error(
"INSUFFICIENT SHARES: You cannot sell more shares than you own.",
);
}
const estimatedRevenue = emoji.currentPrice * amount;
const dropRatePerShare = 0.05;
const targetPrice =
emoji.currentPrice * Math.pow(1 - dropRatePerShare, amount);
const estimatedImpact = Math.max(0.01, targetPrice) - emoji.currentPrice;
logEvent("SELL_STOCK_TRANSACTION", { amount, symbol: emoji.symbol });
const response = await sellStock({
emojiId: emoji.id,
amount: amount,
});
const sellResult = response.data?.sellStock as any;
if (
!sellResult ||
sellResult === 0 ||
(Array.isArray(sellResult) && sellResult.length === 0)
) {
throw new Error("Transaction denied: Insufficient shares.");
}
const actualRevenue = Array.isArray(sellResult)
? sellResult[0].actual_revenue
: estimatedRevenue;
const actualImpact = Array.isArray(sellResult)
? sellResult[0].actual_impact
: estimatedImpact;
// TODO: Optionally add a custom resolver to call AI to generate headline for this sale
};
Query Geospatial Data (Local Radar): In app/src/components/LocalRadar.tsx, we subscribe to the getTrendingEmojisNearMeRef query. The dynamic JSON array from the _select execution maps directly to the UI list, utilizing PostGIS's distance calculations.
import { subscribe } from "@firebase/data-connect";
import { getTrendingEmojisNearMeRef } from "@dataconnect/generated";
// ... inside the component
useEffect(() => {
if (!location) return;
setIsLoadingTrends(true);
// Subscribe to realtime updates for trending emojis within a 50km radius
const unsub = subscribe(
getTrendingEmojisNearMeRef({
userLat: location.lat,
userLng: location.lng,
radiusMeters: 50000, // 50km
}),
(res) => {
if (res.data) setLocalData(res.data);
setIsLoadingTrends(false);
},
(err) => {
console.error("Local Radar Realtime Error:", err);
setIsLoadingTrends(false);
},
);
return () => unsub();
}, [location?.lat, location?.lng]);
Query Geospatial Data (Global Assets Map): In app/src/app/map/page.tsx (the Insights Page), we use Native SQL's complex window functions (RANK() OVER) to find the single most popular emoji for every city in the database.
import { subscribe } from "@firebase/data-connect";
import { getTopEmojisByCityRef, getTrendingEmojisNearMeRef, getUserProfileRef } from "@dataconnect/generated";
// ... inside the component
useEffect(() => {
// Subscribe to realtime updates for the authenticated user's profile and stock ownership
const unsub = subscribe(getUserProfileRef(), (res) => {
if (res.data) setProfileData(res.data);
});
return () => unsub();
}, []);
useEffect(() => {
// Subscribe to realtime updates for top trending emojis partitioned by user city
const unsub = subscribe(getTopEmojisByCityRef(), (res) => {
if (res.data) setCityData(res.data);
});
return () => unsub();
}, []);
useEffect(() => {
setRadarLoading(true);
// Subscribe to realtime updates for trending emojis within a specified geographic radius
const unsub = subscribe(
getTrendingEmojisNearMeRef({
userLat: coords.lat,
userLng: coords.lng,
radiusMeters: radiusKm * 1000,
}),
(res) => {
if (res.data) setRadarData(res.data);
setRadarLoading(false);
},
);
return () => unsub();
}, [coords.lat, coords.lng, radiusKm]);
See it in action
- In your browser, navigate to the Geo page from the top navigation bar.
- If your location is correctly set in your Profile, the Global Top Assets map will ping the
GetTopEmojisByCitynative query to drop pins on cities with high trade volumes. - Click Scan Local Network. The
Local Radar Scannerwill ask for your browser's location and ping theGetTrendingEmojisNearMenative query, utilizing PostGIS to find the top assets specifically traded within 50km of your coordinates! - Navigate to the Home page or Profile page and purchase some assets to see your balance deduct and the emoji price update automatically via your atomic
_executequeries.
8. Realtime subscriptions and caching
In the previous section, we used the subscribe() method in our React components to fetch data. While that successfully retrieved the initial state, a true stock exchange needs to feel alive. If another user buys a massive amount of emoji stock, your screen should update instantly.
This is where Firebase SQL Connect's Realtime features come in.
What is Realtime and how does it work?
Realtime support allows your application to receive proactive notifications from the server whenever data your app is using has been updated.
Here is the underlying mechanism:
- Trigger (
@refresh): You tell the SQL Connect backend which specific mutations should trigger a data refresh for a given query. - Broadcast: When one of those mutations executes (e.g., someone runs
BuyStock), the server proactively broadcasts a realtime notification to any connected clients listening to that query. - Cache Update: When the notification arrives, the JS SDK treats it just like an ad-hoc query execution. The local cache is instantly updated with the new data.
- UI Reactivity: The SDK automatically fires the
onNextcallbacks for all active subscribers, causing your React state to update and your UI to re-render "in real time".
Add @refresh triggers to your queries
To enable this on the backend, we need to add the @refresh directive to our queries.
- Open
dataconnect/friendly-exchange/queries.gql. - Update your existing queries by attaching
@refreshdirectives for every market-altering mutation. For example, updateGetDashboardDataandGetUserProfile:
# Get dashboard data including top emojis by price and recent market events
query GetDashboardData
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
)
@refresh(onMutationExecuted: { operation: "BuyStock" })
@refresh(onMutationExecuted: { operation: "SellStock" })
@refresh(onMutationExecuted: { operation: "TriggerEvent" })
@refresh(onMutationExecuted: { operation: "MarketMakerTrade" }) {
emojis(orderBy: [{ currentPrice: DESC }]) {
id
symbol
name
description
currentPrice
trend
}
events(orderBy: [{ createdAt: DESC }], limit: 15) {
id
description
impact
createdAt
user {
username
profileImage
}
emoji {
symbol
}
}
}
# Get current authenticated user profile and their stock ownership using auth.uid
query GetUserProfile
@auth(level: USER)
@refresh(onMutationExecuted: { operation: "BuyStock" })
@refresh(onMutationExecuted: { operation: "SellStock" })
@refresh(onMutationExecuted: { operation: "UpdateUserLocation" })
@refresh(onMutationExecuted: { operation: "UpdateUserRole" }) {
user(id_expr: "auth.uid") {
points
username
profileImage
role
stockOwnerships_on_user {
shares
emoji {
id
symbol
currentPrice
name
}
}
city
latitude
longitude
}
}
Key Takeaways
@refresh(onMutationExecuted: ...): Instructs the server to re-evaluate this query and push new data to subscribers whenever the specified mutation occurs.
Generate the SDK
Because you have defined new queries and mutations in your GraphQL files, you must run the SDK generator so your TypeScript frontend can call it.
Open your terminal and run:
firebase dataconnect:sdk:generate
Handle Realtime Subscriptions in the Web App
We already laid the groundwork for this in the previous section by using the subscribe method. Let's look closer at how the generated SDK handles this in React.
If you open app/src/app/page.tsx (the Home page), you will see the useEffect hook managing the dashboard data:
import { subscribe } from "@firebase/data-connect";
import { getDashboardDataRef } from "@dataconnect/generated";
// ... inside the component
useEffect(() => {
const queryRef = getDashboardDataRef();
// The subscribe function registers the QueryRef and callbacks
const unsubscribe = subscribe(
queryRef,
(res) => {
// onNext: Fires initially, AND whenever a @refresh trigger occurs
if (res.data) setDashboardData(res.data);
setIsDashboardLoading(false);
},
(err) => {
// onError: Handles any server or permission errors
console.error("Dashboard Realtime Error:", err);
setIsDashboardLoading(false);
}
);
// onComplete/Cleanup: Unregisters the callbacks when the component unmounts
return () => unsubscribe();
}, [user]);
Key Takeaways
subscribe(queryRef, onNext, onError): Enables Realtime notifications for the specificQueryRef.unsubscribe(): Callingsubscribereturns a cleanup function. It is critical to return this in youruseEffectso that when the component unmounts (e.g., the user navigates away), the subscription is canceled and memory leaks are prevented.- Caching Efficiency: If multiple components subscribe to the same query (like
GetDashboardData), the SDK shares the cached result. When a Realtime notification arrives, the cache updates once, and all active subscribers are notified automatically.
See it in action
Because you've added @refresh to your backend and subscribe to your frontend, your app is now fully reactive.
- Open your web app in two separate browser windows side-by-side.
- In one window, purchase a few shares of an emoji.
- Watch the second window—without refreshing the page, you will instantly see the emoji's price increase!
9. Conclusion
Congratulations, you've successfully built and deployed a realtime, highly complex trading platform directly on top of PostgreSQL using Firebase SQL Connect!
By utilizing SQL Connect, you were able to:
- Define a strictly-typed GraphQL schema that maps directly to PostgreSQL.
- Enforce granular, row-level security using Firebase Authentication and @auth directives.
- Leverage advanced Native SQL to query geospatial data with PostGIS and write atomic market transactions via CTEs.
- Make your entire application reactive using the @refresh directive for realtime subscriptions.
- Seamlessly generate frontend SDKs to keep your client code synced with your database.
If you want to play with your own market data, feel free to insert your own mock emojis, locations, and pricing histories using the Firebase SQL Connect extension by mimicking the .gql seed files, or add them through the SQL Connect execution pane in VS Code.
10. Deploy to Cloud
Now that you've worked through the local development iteration, it's time to deploy your schema, data, and queries to the server. This can be done using the Firebase SQL Connect VS Code extension or the Firebase CLI.
Set up Firebase Authentication in your Firebase project
- Set up Firebase Authentication with Google Sign-In.
- (Optional) Allow domains for Firebase Authentication using the Firebase console (for example,
http://127.0.0.1).- In the Authentication settings, go to Authorized Domains.
- Click "Add Domain" and include your local domain in the list.
Enable required PostgreSQL Extensions
Because this app utilizes PostgreSQL extensions for vector search and location tracking, you must manually enable them on your provisioned Cloud SQL instance before deploying your schema.
- Navigate to the Google Cloud Console.
- Go to Cloud SQL -> select your provisioned instance -> click Cloud SQL Studio.
- Log into your database and execute the following commands:
# Required for the Geo Map page
CREATE EXTENSION IF NOT EXISTS postgis;
# Required for Vector Search
CREATE EXTENSION IF NOT EXISTS "vector";
# Required for automatic Vector Search embedding generation
CREATE EXTENSION IF NOT EXISTS "google_ml_integration";
Build your web app for hosting
Back in VS Code, ensure you have placed your firebaseConfig variables in lib/firebase.tsx (as done in the setup section).
Next, guarantee that your frontend is using the latest generated hooks by running:
firebase dataconnect:sdk:generate
Then, build the React web app for hosting deployment:
npm run build
Deploy with the Firebase CLI
In dataconnect/dataconnect.yaml, ensure that your instance ID, database, and service ID match your actual Google Cloud project identifiers, and use the v1 specification:
specVersion: v1
serviceId: your-project-id-service
location: us-west4
schemas:
- source: ./schema
datasource:
postgresql:
database: your-project-id-database
cloudSql:
instanceId: your-project-id-instance
connectorDirs:
- ./friendly-exchange
In your terminal, run the following command to deploy:
firebase deploy --only dataconnect,hosting
For updates or refactors, run this command to compare your schema changes:
firebase dataconnect:sql:diff
If the changes are acceptable, apply them with:
firebase dataconnect:sql:migrate
Your Cloud SQL for PostgreSQL instance will be updated with the final deployed schema and data. You should now be able to see your app live at your-project.web.app/.
Learn more
11. Optional: Vector search with Firebase SQL Connect (billing required)
In this section, you'll enable vector search in your emoji exchange using Firebase SQL Connect. This feature allows for semantic, content-based searches, such as finding emojis that match a vibe or concept using vector embeddings.
This step requires that you completed the last step of this codelab to deploy to Google Cloud.
Update the schema to include embeddings for a field
In dataconnect/schema/schema.gql, add the descriptionEmbedding field to your Emoji table. Replace your existing Emoji type with this updated version:
# Emojis
# emoji-stockOwnership is a one-to-many relationship, emoji-priceHistory is a one-to-many relationship
# Implements @searchable directives for full-text search
# Optional: implements Vector type for semantic search
type Emoji @table {
id: UUID! @default(expr: "uuidV4()")
symbol: String!
name: String! @searchable
tags: [String!]
description: String! @searchable
descriptionEmbedding: Vector @col(size: 768)
currentPrice: Float! @default(value: 10.0)
trend: Float! @default(value: 0.0)
}
Key Takeaways
descriptionEmbedding: Vector @col(size: 768): This field stores the semantic embeddings of your emoji descriptions, enabling vector-based content search in your app.
Add a vector search query
In dataconnect/friendly-exchange/queries.gql, add the following query to perform vector searches:
# Search emoji descriptions using Vertex AI embeddings
query VectorSearchEmojis($query: String!)
@auth(
level: PUBLIC
insecureReason: "This operation is safe to expose to the public."
)
@refresh(onMutationExecuted: { operation: "BuyStock" })
@refresh(onMutationExecuted: { operation: "SellStock" })
@refresh(onMutationExecuted: { operation: "TriggerEvent" })
@refresh(onMutationExecuted: { operation: "MarketMakerTrade" }) {
emojis_descriptionEmbedding_similarity(
compare_embed: { model: "text-multilingual-embedding-002", text: $query }
method: COSINE
within: 2
limit: 15
) {
id
symbol
name
description
currentPrice
trend
_metadata {
distance
}
}
}
Key Takeaways:
compare_embed: Specifies the embedding model (text-multilingual-embedding-002) and the input text ($query) for comparison.method: Specifies the similarity method (COSINE), measuring the cosine similarity between the vectors.within: Limits the search to emojis with a distance of 2 or less, focusing on close content matches.
Generate the SDK
Because you have defined new queries and mutations in your GraphQL files, you must run the SDK generator so your TypeScript frontend can call it.
Open your terminal and run:
firebase dataconnect:sdk:generate
Activate Vertex AI and re-deploy
- Follow the prerequisites guide to set up Vertex AI APIs from Google Cloud. This step is essential to support the embedding generation.
- Re-deploy your schema to activate
pgvectorand vector search by runningfirebase deploy --only dataconnector clicking "Deploy to Production" using the Firebase SQL Connect VS Code extension.
Populate the database with embeddings
- Open the
dataconnectfolder in VS Code. - Click Run (Production) in
optional_vector_seed.gqlto populate your deployed database with the 768-dimensional embeddings for the emojis.
Implement the vector search function in your app
Now that the schema and query are set up, integrate the vector search into your app's frontend.
In app/src/app/page.tsx (your Home component), review the useEffect that listens to the search input and swaps dynamically between full-text search and vector search based on the user's selected searchMode:
import { subscribe } from "@firebase/data-connect";
import {
getDashboardDataRef,
searchEmojisRef,
vectorSearchEmojisRef, // <-- Add this!
getChronologicalTickerRef,
getUserProfileRef,
} from "@dataconnect/generated";
// Inside Home component, find the search useeffect
useEffect(() => {
if (!debouncedSearch) {
setSearchData(null);
return;
}
let unsubscribe: () => void;
if (searchMode === "TEXT") {
// Subscribe to realtime full-text search results for emojis based on user input
unsubscribe = subscribe(
searchEmojisRef({ query: debouncedSearch }),
(res) => {
if (res.data) setSearchData(res.data.emojis_search);
setIsSearchLoading(false);
},
(err) => {
console.error("Text Search Error:", err);
setIsSearchLoading(false);
},
);
} else {
// Subscribe to realtime vector search results using semantic similarity for emojis based on user input
unsubscribe = subscribe(
vectorSearchEmojisRef({ query: debouncedSearch }),
(res) => {
if (res.data)
setSearchData(res.data.emojis_descriptionEmbedding_similarity);
setIsSearchLoading(false);
},
(err) => {
console.error("Vector Search Error:", err);
setIsSearchLoading(false);
},
);
}
return () => {
if (unsubscribe) unsubscribe();
};
}, [debouncedSearch, searchMode]);
See it in action
Navigate to the search bar on your app's homepage. Type in abstract phrases like "happy", "nature", or "technology". Toggle the search mode from TEXT to VECTOR and notice how the results shift from exact string matches to contextual, semantic matches returned directly from Vertex AI and PostgreSQL!
12. Optional: Custom Resolvers with Vertex AI (billing required)
10:00
By writing Custom Resolvers, you can extend Firebase SQL Connect to support other data sources and combine them into your unified GraphQL schema. In this section, you'll write a Firebase Cloud Function that uses Vertex AI (Gemini) to generate a satirical financial news headline whenever a user makes a large trade, and expose that function through SQL Connect.
Initialize the custom resolver
Instead of creating all the boilerplate files manually, the Firebase CLI has a built-in generator for custom resolvers.
Open your terminal in the root of your project and run:
firebase init dataconnect:resolver
When prompted by the CLI:
- Enter
generateTradeHeadlineas the name for your custom resolver. - Select TypeScript to generate the example implementation.
The CLI will automatically create a new dataconnect/schema_generateTradeHeadline/schema.gql file, initialize a functions directory with sample code, and link the resolver in your dataconnect.yaml configuration!
Define the custom resolver schema
Next, you need to define the exact shape of your custom endpoint using a GraphQL schema.
Open the newly generated dataconnect/schema_generateTradeHeadline/schema.gql file and replace its contents with the following code:
# Custom resolver fields can be defined on root Query and Mutation types.
type Mutation {
# This field will be backed by your Cloud Function.
generateTradeHeadline(
emojiSymbol: String!
emojiName: String!
username: String!
tradeAmount: Int!
tradeCost: Float!
tradeType: String!
): String!
}
Key Takeaways:
- By placing this inside the root
type Mutation, you are telling SQL Connect that this operation might have side-effects (like calling an AI API) rather than just reading data.
Implement the custom resolver logic
Next, implement your resolver using Cloud Functions. Under the hood, you are creating a GraphQL server; however, Cloud Functions provides a helper method, onGraphRequest, that handles the boilerplate so you only need to write the core logic.
Open your Firebase Functions file (functions/src/index.ts), which the CLI generated for you. Replace the entire file with the Gemini API implementation:
import { setGlobalOptions } from "firebase-functions";
import {
FirebaseContext,
onGraphRequest,
} from "firebase-functions/dataconnect/graphql";
import { initializeApp, getApps } from "firebase-admin/app";
import { GoogleGenAI } from "@google/genai";
setGlobalOptions({
maxInstances: 10,
region: "us-west4",
});
if (getApps().length === 0) {
initializeApp();
}
const ai = new GoogleGenAI({
vertexai: true,
project: process.env.GCLOUD_PROJECT || "your-project-id",
location: process.env.GCLOUD_LOCATION || "us-west4",
});
const headlineOpts = {
// Points to the schema you defined earlier
schemaFilePath: "dataconnect/schema_generateTradeHeadline/schema.gql",
resolvers: {
mutation: {
// Generate a satirical financial news headline for a stock trade using Vertex AI
async generateTradeHeadline(
_parent: unknown,
args: Record<string, unknown>,
_contextValue: FirebaseContext,
_info: unknown,
): Promise<string> {
const {
emojiSymbol,
emojiName,
username,
tradeAmount,
tradeCost,
tradeType,
} = args;
try {
const prompt = `You are a hype-driven, satirical financial news bot.
A user named '${username}' just executed a massive ${tradeType} of ${tradeAmount} shares of ${emojiSymbol} (${emojiName}) for $${tradeCost}.
Write a single, punchy, dramatic news headline (under 12 words) about this market move, use puns wherever possible, but don't round or exagerate the numbers. Include the asset symbol.`;
const response = await ai.models.generateContent({
model: "gemini-2.5-flash-lite",
contents: prompt,
});
if (!response.text) {
throw new Error("No text returned from Vertex AI");
}
return response.text.trim();
} catch (error) {
console.error("Vertex AI generation failed:", error);
return `BREAKING: Massive ${tradeType} detected on ${emojiSymbol}! Market reacting.`;
}
},
},
},
};
export const generateTradeHeadline = onGraphRequest(headlineOpts);
Điểm chính cần ghi nhớ:
onGraphRequest: Một trình bao bọc Hàm Firebase chuyên biệt, giúp ánh xạ một Cloud Function với lược đồ SQL Connect Custom Resolver.args: Các đối số được truyền từ đột biến GraphQL sẽ tự động được nhập và trích xuất tại đây để được chèn vào câu lệnh cho Gemini.
Thêm đột biến vào trình kết nối
Giờ đây, khi logic trình phân giải tuỳ chỉnh đã tồn tại, hãy hiển thị logic đó thông qua trình kết nối của ứng dụng để giao diện người dùng có thể gọi logic đó.
Mở dataconnect/friendly-exchange/mutations.gql rồi thêm đột biến:
# Generate an AI headline for a stock trade
mutation GenerateTradeHeadline(
$emojiSymbol: String!
$emojiName: String!
$username: String!
$tradeAmount: Int!
$tradeCost: Float!
$tradeType: String!
)
@auth(
level: USER
insecureReason: "This operation is safe to expose to any authenticated user."
) {
aiHeadline: generateTradeHeadline(
emojiSymbol: $emojiSymbol
emojiName: $emojiName
username: $username
tradeAmount: $tradeAmount
tradeCost: $tradeCost
tradeType: $tradeType
)
}
Triển khai và tạo SDK
Vì Trình phân giải tuỳ chỉnh chạy thông qua Cloud Functions, nên bạn phải triển khai các hàm của mình lên Google Cloud để điểm cuối hoạt động.
Mở cửa sổ dòng lệnh rồi triển khai hàm:
firebase deploy --only functions
Sau khi triển khai, hãy tạo SDK giao diện người dùng để đưa đột biến AI mới vào:
firebase dataconnect:sdk:generate
Tích hợp Trình phân giải AI vào ứng dụng web
Hãy thiết lập để mọi giao dịch từ 10 cổ phiếu trở lên đều kích hoạt cảnh báo tin nóng!
Mở lib/ExchangeService.tsx. Trước tiên, hãy đảm bảo bạn nhập generateTradeHeadline và triggerEvent ở trên cùng:
import {
buyStock,
sellStock,
generateTradeHeadline,
triggerEvent
} from "@dataconnect/generated";
Tiếp theo, hãy cuộn xuống cuối hàm executeBuyStock và thay thế TODO bằng khối kích hoạt AI ngay trước khi hàm kết thúc:
// ... (existing executeBuyStock code)
const actualImpact = Array.isArray(buyResult)
? buyResult[0].actual_impact
: estimatedImpact;
if (amount >= 10 && user) {
setTimeout(() => {
logEvent("GENERATE_HEADLINE_RESOLVER");
}, 2000);
const headlineResult = await generateTradeHeadline({
emojiSymbol: emoji.symbol,
emojiName: emoji.name,
username: user.displayName || "Anonymous Whale",
tradeAmount: amount,
tradeCost: actualCost.toFixed(2),
tradeType: "BUY",
});
await triggerEvent({
emojiId: emoji.id,
impact: actualImpact.toFixed(2),
description: `GEMINI REPORT: ${headlineResult.data?.aiHeadline}`,
now: new Date().toISOString(),
});
}
};
Làm chính xác như vậy ở cuối hàm executeSellStock:
// ... (existing executeSellStock code)
const actualImpact = Array.isArray(sellResult)
? sellResult[0].actual_impact
: estimatedImpact;
if (amount >= 10 && user) {
const headlineResult = await generateTradeHeadline({
emojiSymbol: emoji.symbol,
emojiName: emoji.name,
username: user.displayName || "Anonymous Whale",
tradeAmount: amount,
tradeCost: actualRevenue.toFixed(2),
tradeType: "SELL",
});
await triggerEvent({
emojiId: emoji.id,
impact: actualImpact.toFixed(2),
description: `GEMINI REPORT: ${headlineResult.data?.aiHeadline}`,
now: new Date().toISOString(),
});
}
};
Ví dụ thực tế
- Tải lại ứng dụng web.
- Đảm bảo bạn đã đăng nhập và có đủ tiền.
- Chọn một biểu tượng cảm xúc và mua từ 10 phần quà trở lên cùng một lúc.
- Xem Bảng tin thị trường toàn cầu ở bên phải trang tổng quan. Trong vòng vài giây, bạn sẽ thấy một tiêu đề tin tức châm biếm tuỳ chỉnh do Gemini tạo!