Skip to Content
核心概念公開讀取

公開讀取:把單一檢視公開給匿名讀者

公開讀取權杖是本模組唯一不需要登入的讀取介面。版主公開的是某一張表的某一個已存檢視,拿到權杖 URL 的人(權杖不是 secretless 時還要有它的 Bearer 密鑰)不需要 TeamSync 登入狀態就能讀那個檢視。除此之外什麼都碰不到:其他檢視不行、其他資料表不行、資料列的建立者不行、資料表的規則與權限設定也不行。

公開是一個明確的動作,也有明確的影響範圍。這一頁的目的,就是讓你在把 URL 交出去之前先知道那個範圍有多大。

鑄造:版主公開一個檢視

curl -X POST \ "$BASE_URL/private/module/custom_tables/chatroom/tables/22222222-2222-4222-8222-222222222222/public-read-tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "公開價目表", "view_id": "33333333-3333-4333-8333-333333333333", "secretless": true, "visible_columns": ["品項", "單價"], "read_filter": { "and": [{ "column": "col_9c1e2f3a_4b5c_4d6e_8f70_819203a4b5c6", "op": "eq", "value": true }] }, "allow_query": false, "rpm": 120, "valid_until": "2026-12-31T23:59:59" }'

這個 URL 有兩件事常讓人踩坑。它沒有 scope id 這一段:路徑是 /chatroom/tables/{table_id}/...,不是 /chatroom/{chatroom_id}/tables/...,因為版主權限檢查只用 table_id 就能推出範圍。同樣形狀在 /department/ 與 /company/ 底下也有。只有資料表版主能鑄造:該表明確指定的版主;聊天室資料表另外包括聊天室建立者、該聊天室所屬部門的部門管理者(role ≥ 2)或公司管理者(role ≥ 3);部門資料表為該部門的管理者(role ≥ 2)或公司管理者;公司資料表為公司管理者。visible_columns 可用顯示名稱或內部鍵值;read_filter 則是一般的 row policy 文法,必須使用內部 col_<hex> 鍵值(這裡是「已上架」欄位)。

回應是密鑰唯一會出現的一次:

{ "token_id": "44444444-4444-4444-8444-444444444444", "secret": null, "url_path": "/public/module/custom_tables/read/44444444-4444-4444-8444-444444444444/records", "table_id": "22222222-2222-4222-8222-222222222222", "view_id": "33333333-3333-4333-8333-333333333333", "allow_query": false, "rpm": 120, "secretless": true, "valid_until": "2026-12-31T23:59:59" }

visible_columns、read_filter 與 revoked 不在鑄造回應中;權杖清單會回顯它們。

無密鑰或帶密鑰

  • secretless: true 不會存任何密鑰雜湊。URL 裡的 uuid 本身就是全部的憑證。 請把那個 URL 當成密碼看待:它傳到哪裡,那裡的人就能讀這個檢視。
  • secretless: false(預設)會產生一個 Bearer 密鑰,只在鑄造回應裡出現一次,資料庫只存它的 SHA-256。之後讀者要帶 Authorization: Bearer <secret>。比對是固定時間的,且不存在的 token id 也會花掉同樣的計算成本,所以時間差不會洩漏權杖是否存在。

拿到密鑰請立刻存進密鑰管理系統。沒有「再看一次」的路由,而列表回應裡的 visible_columns 回的是內部 col_<hex> 鍵,永遠不會回密鑰或它的雜湊。

匿名讀者會拿到什麼

每一列就是這些,不多不少:

{ "id": "55555555-5555-4555-8555-555555555555", "created_at": "2026-07-25T01:00:00", "updated_at": "2026-07-25T01:05:00", "data": { "品項": "升降桌", "單價": 12800 } }

再加上 total、table_name,以及 columns({name, type}),所以前端不必呼叫任何私有 API 就能畫出表頭。data 的鍵是顯示名稱,而舊 schema 留下的殘餘鍵會被丟掉。

刻意不給的有:created_by、created_by_client、pending_approval、sort_order、聊天室/部門/公司 id、資料表 id,以及資料表的 settings、rules、default_permissions、column_acl、client_access。被軟刪除的資料列永遠不會出現。在公開通道上,id、created_at、updated_at 是呼叫端唯一可以在過濾或排序中引用的系統欄位,所以匿名讀者無法拿你的表按建立者排序。

可見欄位的規則

實際公開的欄位是檢視自己的 columns 與權杖 visible_columns 的交集,再扣掉模組拒絕公開的部分:

欄位種類在公開通道上
string、text、數值、boolean、日期、選單、json落在交集裡就會公開
decimal與其他純量一樣公開,但每一格都是依欄位 scale 補齊的精確十進位字串("12800.00"),絕不是 JSON 數字;columns[].type 為 "decimal",不會公開 precision 與 scale
attachment、link、rollup、lookup永不公開。鑄造時指定會得到 422,讀取端也會強制隱藏
principal、user、social_client預設隱藏——這道閘門是從 principal 家族推導的,所以新的 principal 型別會自動繼承。只有明確指定才公開,而且只給原始儲存字串(principal cell 會保留 user:/smc:/room: 標籤),不做任何身分解析
formula會公開,但只以同表純量為輸入計算:所有被隱藏或跨表的輸入都會解析成 null。以十進位計算的 formula 會精確求值,並以十進位字串公開
僅管理者可見的欄位(column_acl)永不公開

最後一列的 formula 規則在你想公開「計算後的價格或分數」時特別重要:如果那個公式讀的是 rollup,公開出去的值會是 null,不是數字。宣傳之前先看一眼實際輸出。

哪些東西永遠不能公開

  • 受 SCP channel 規則治理的資料表。 鑄造會失敗並回 409 Cannot publish a table governed by a scoped channel policy。公開讀者沒有「目前所在聊天室」,所以讀取時 channel 門檻還會再以「缺少綁定就拒絕」的方式套一層保險。
  • 帶跨表計算語法的檢視:computed_filters 非空,或 sort_by / sort 的鍵指到 link、rollup、lookup、formula 欄位。鑄造時是 422,而且每次讀取都會重新檢查。已經公開之後才被改成這種形狀的檢視會停止服務,改回統一的 404。
  • visible_columns 指到資料表沒有的欄位(422 visible_columns references unknown columns: […]),或上面被拒絕的那幾種欄位(422)。read_filter 中出現未知欄位,或綁定的檢視其儲存設定已無法通過驗證,則是 400。

讀取

# 列表 curl "$BASE_URL/public/module/custom_tables/read/$TOKEN_ID/records?skip=0&limit=50" # 單一資料列 curl "$BASE_URL/public/module/custom_tables/read/$TOKEN_ID/records/$RECORD_ID" # 進一步收窄,只有鑄造時帶 allow_query: true 才可用 curl -X POST "$BASE_URL/public/module/custom_tables/read/$TOKEN_ID/query" \ -H "Content-Type: application/json" \ -d '{ "filters": { "col_aaaaaaaa_aaaa_4aaa_8aaa_aaaaaaaaaaaa": "桌" }, "sort": [{ "column": "col_bbbbbbbb_bbbb_4bbb_8bbb_bbbbbbbbbbbb", "order": "asc" }], "limit": 20 }'

limit 上限是 100。權杖帶密鑰時要加上 -H "Authorization: Bearer $SECRET"。

/query body 精確對應 PublicQueryRequest,未知鍵一律禁止:

欄位契約
filters選填的 legacy filter map,從欄位鍵(internal col_<hex> ID;顯示名稱也會被對應)直接對應到值:string/text 以不分大小寫的子字串比對(值裡含 % 或 _ 時改當成 LIKE 樣式),multi_select 的值會命中清單中含有該選項的列,其他型別完全相等(decimal 的值必須等於儲存的、補齊 scale 的字串)。唯一接受的 dict 值是 created_at/updated_at 上的 {"gte", "lte"} 區間;需要 operator 請用 stored_filters
stored_filters選填,最多 20 個 typed stored/link predicate;需要明確 {column, op, value} operation 時使用此欄位。decimal 欄位的 operand 必須是十進位字串("12.50");JSON 數字會得到 400 decimal filter value must be a decimal string,比較方式為精確比較
any_of選填,1–10 個 OR group;每組內的 predicate 以 AND 組合
q選填,只搜尋可見 string/text 欄位、不分大小寫,最多 200 字元
sort_by、sort_order選填,單一欄位鍵(internal ID 或顯示名稱)或 system key,加上 asc 或 desc
sort選填,{column, order} 的多欄排序;優先於 sort_by。最多 3 個鍵,和私有搜尋一致:自後端 5.22.0 起,更長的清單會回 400,其 detail 以 invalid body: 開頭,並指出 sort 與最多 3 個項目的上限。5.21.0 以前 model 允許到 10 個鍵,其中 4–10 個鍵會回 500 Internal Server Error
skip大於等於 0 的整數,預設 0
limit1–100 的整數,預設 50

這個 model 刻意沒有 computed_filters,也沒有任何 aggregate member。空 body 合法,會變成帶上述預設值的 PublicQueryRequest()。伺服器先讀取 raw request body:剛好 16,384 bytes 合法,任何更大的 body 都在 JSON parse 之前回 413。JSON 無效或不符合 model 則回 400,不是 FastAPI 常見的 422。

查詢通道的資料列選取只能收窄。呼叫端的 filters、stored predicates、OR groups 與文字搜尋都會 AND 在檢視的資料列條件上,因此無法放寬或移除已發布的資料列切片;若有提供,排序與分頁則由呼叫端決定:

  • filters 的鍵撞在一起時,檢視的值贏。
  • 呼叫端的 sort(或 sort_by)會取代檢視的排序;sort 優先於 sort_by。
  • skip 與 limit 由呼叫端決定。
  • 請求主體禁止未知鍵,結構上也沒有 computed_filters、沒有任何彙總成員,所以公開呼叫端無法注入跨表條件或彙總。
  • q 是不分大小寫的子字串搜尋,只搜可見的 string 與 text 欄位,最長 200 字。
  • stored_filters 與 any_of 只接受 internal col_<hex> column ID(在這裡寫顯示名稱會被當成未知欄位);filters、sort_by 與 sort 也接受 response data 中的顯示名稱,並會對應成 internal ID(排序另接受明確列出的 system keys)。一律優先用 internal ID:欄位改名後仍然有效。

引用權杖沒有公開的欄位,回的錯誤與引用一個真的不存在的欄位完全相同(Column 'X' does not exist in table schema)。沒有「存在但被隱藏」的探測管道。

權杖上的 row policy

權杖可以帶 read_filter,它就是一般的 row policy,但在沒有主體的情況下解析:

  • $today、$today±Nd、$now 正常解析,所以「本週班表」或「目前促銷」不需要有人定期改權杖就會一直正確。
  • $me 與 $me.department 在鑄造時就會被 422 拒絕:公開讀取沒有行為主體,這種 leaf 只可能解析成永不命中;鑄造時的檢查把一個默默變空的權杖轉成明確的錯誤。
  • Predicate 可以指向 decimal 欄位;operand 請給十進位字串,在清單 SQL 與單筆重新檢查中都以精確方式比較。

沒有 read_filter 時,權杖讀整個檢視;有的時候,就讀被政策收窄後的檢視,且身分不是管理者。

所有權杖、檢視與資料列的失敗都是同一個 404

不存在的 token id、已撤銷、已過期、缺少或錯誤的 Bearer 密鑰、allow_query: false 卻打 /query、資料表被刪除、檢視被刪或被改成不能公開的形狀、檢視的過濾或排序現在指到被隱藏的欄位、單列查詢被檢視或 row policy 濾掉:全部都回 404 {"detail":"Not found"}。

這種一致性正是重點。公開 URL 不應該告訴匿名探測者:權杖存不存在、是不是過期了、還是那筆資料只是被隱藏。代價是你少了一條除錯路徑,所以請改從私有側除錯:

  1. 讀那張表的權杖列表。它包含已撤銷的權杖(revoked: true),還有 valid_until、rpm、allow_query、last_used_at。
  2. 打開綁定的檢視,確認它仍然沒有跨表計算語法、也沒有指到被隱藏的欄位。
  3. 請求錯誤保有自己的狀態碼:GET …/records 的 skip/limit 查詢參數超出範圍或路徑 id 超過 36 字元為 422,/query body 不合法(包括超出範圍的 skip/limit,在那裡它們是 body 欄位)或呼叫者引用了欄位(未知與被隱藏的欄位共用同一則訊息)為 400,/query body 超過 16,384 位元組為 413。流量限制回 429。有一種檢視問題也不會被遮蔽:綁定的檢視若以記錄引擎無法排序的欄位排序——json 欄位,或明確公開的 user/social_client/principal 欄位——每次讀取都會回記錄引擎的 400。

流量限制、活躍度、有效期

每個權杖有自己的 rpm(1 到 600,預設 60),以每權杖固定 60 秒視窗計算。超過會回 429 rate limit exceeded (N/min per token)。Redis 不可用時這道閘門會 fail open,因為它是防濫用機制,不是安全邊界。不要為了多一點額度再鑄一個權杖,那只會讓你要撤銷的表面積變兩倍。

last_used_at 每個權杖最多每分鐘更新一次,所以請把它讀成「還有人在用嗎」的粗略訊號,不要當成點擊計數。

valid_until 在兩條鑄造路徑上都會正規化成 naive UTC,所以輸入 2026-12-31T23:59:59+08:00 會存成等值的 UTC 時刻,不管你怎麼傳都指同一個瞬間。null 表示不過期。

撤銷是永久的

curl -X DELETE \ "$BASE_URL/private/module/custom_tables/chatroom/tables/$TABLE_ID/public-read-tokens/$TOKEN_ID" \ -H "Authorization: Bearer $TOKEN"

撤銷是墓碑:資料列會留下並標記 revoked: true,URL 永遠失效。沒有任何機制能把它重新啟用,IaC 也不行。不過這塊墓碑並不是永久紀錄:刪除綁定的檢視或 purge 資料表,會連帶刪除它的權杖列(不論已撤銷或仍有效),清單也不會回顯撤銷時間。要輪替就是:鑄造新權杖、部署、用新權杖驗證讀得到,最後撤銷舊的。

Warning 無密鑰的 URL 沒辦法「收回」,只能撤銷。如果無密鑰 URL 流到你沒預期的地方,請當成資料已經公開處理:撤銷權杖再重新鑄造。只換密鑰沒有用,因為對無密鑰權杖來說,uuid 就是密鑰。

用 IaC 宣告已公開的權杖

public_read 行類型 讓「公開」這件事進版控:

{"kind":"public_read","table":"prices","ref":"public_price_list","spec":{"view":"published_items","secretless":true,"rpm":120,"allow_query":false}}

有四條規則要記住。IaC v1 只支援無密鑰,因為只顯示一次的 Bearer 密鑰無法在文件裡來回;要帶密鑰的權杖請走 REST 鑄造。view 必須指向受管理的檢視:來自前一次 apply 的狀態,或同一份文件裡的 view 行。更新是就地更新,所以重複 apply 時 token id 與憑證 URL 都保持不變。而 state: "absent" 會撤銷線上的權杖,那和 REST 撤銷一樣是永久墓碑。

每次 apply 都會用與 REST 相同的驗證程式碼重跑完整的鑄造政策,所以 IaC 不可能公開出 REST 會拒絕的東西。

端點的完整請求與回應合約見 公開讀取權杖;反方向的寫入請見 外部回呼。

Last updated on