附件型別:attachment
用途
attachment 適合發票、合約、商品照片與檢驗報告等檔案欄位。例如採購單可把 PDF 發票與簽收照片放在「發票附件」cell;檔案本身先上傳成 blob,紀錄只保存該 blob 的 ID。
建立 schema
columns.create 可直接接受附件限制;建立整張表時,schema_definition.columns 也使用相同欄位:
{
"name": "發票附件",
"type": "attachment",
"required": false,
"max_count": 3,
"max_file_bytes": 10485760,
"allowed_mime_types": ["application/pdf", "image/png"],
"description": "發票 PDF 或簽收照片"
}max_count 為 1–100;max_file_bytes 必須大於零且最多 5 GiB;allowed_mime_types: null 與空清單都代表不限制。
這三項限制是建立時設定。PATCH columns/{column_id} 使用的 ColumnUpdate 沒有附件限制欄位,因此建立後不能新增、修改或移除限制:送出其中任何一項的 PATCH 會得到 422(extra_forbidden),而且不會變更任何東西。若一定要更改,必須先評估刪欄會移除已儲存的 blob-ID cell,再刪除並重建欄位。
合法與不合法的值
先從 attachments.multipartInit 開始分段上傳並呼叫 attachments.multipartComplete,再把完成回應的 blob.id 陣列寫入 cell:
{
"data": {
"發票附件": [
"44444444-4444-4444-8444-444444444444",
"55555555-5555-4555-8555-555555555555"
]
}
}選填附件欄可用 null 或 [] 表示沒有檔案。明確送出的 null 會以 [] 儲存並回傳,從未寫入過的附件 cell 讀回則是 null。單一 ID 字串也會被當成單元素陣列接受,重複的 ID 會在檢查 max_count 之前被去除。
非字串 ID、不可存取的 blob,或超過已宣告 max_count 的陣列都會以 400 被拒。不存在的 ID、其他公司的 blob,以及未授權的同公司來源都得到相同 detail Unknown or inaccessible attachment ids: [...];公司計費歸屬本身不授予附件權限。內部成本帳本物件的 ID 也以同樣方式拒絕。這個 cell 失敗是因為它的 ID 不是字串:
{
"data": {
"發票附件": [44444444]
}
}建立紀錄時也不能使用差量物件;包含 add 與 remove 的物件只適用於更新,而且 max_count: 1 的單檔欄只允許整組取代。
附件來源權限
已合併的 CT #1994 對 create/update、bulk 與 prepared approval 寫入加入來源閘門。ID 必須屬於該表的公司,且是這個資料列同一 cell已持有、由本表自己的上傳路徑產生,或屬於伺服器綁定 acting conversation 的檔案。保留 ID 不授權複製到其他 cell;MIME、大小與數量檢查仍適用。
Acting user 還可使用 chatroom-scope table 自有聊天室的檔案,但需可讀該聊天室;或複製同公司 live、可讀 CT record 的可見附件欄持有的檔案。Table grant、row ACL 與 acting-room ceiling 仍適用。其他可讀 CT 表產生但尚未存入紀錄的上傳,僅在有界探測證明沒有紀錄持有時允許。Soft-deleted holder 不可讀,但仍阻止這種 pending-upload 例外。
這些基於 user 讀取權的路徑不適用 external client;public callback 與 system/無 actor 寫入不借用 token 作者的讀取權。Fallback probe 每個 blob 最多檢查 1,000 個候選 table schema、40 張 attachment table 與 20 個可能持有的紀錄。不完整探測不能證明檔案未持有,除非找到可讀 holder,否則 fail closed。任意同公司 ID/URL 不是權限。Download 授權與更嚴格的channel-delivery 來源規則各自獨立;這是已合併來源契約,不代表部署或本文件 lane 的行為驗收。
自後端 5.22.0 起多了一條放行路徑,適用於這些呼叫者以及該 Command 的任何其他呼叫者。version 2 的寫入型 Command 可以帶 attachment_copies grant,讓該 Command 把它自己的 select 從指定附件欄位讀到的檔案,複製到指定的目的欄位,並以 grant 作者(而不是呼叫者)的權限來判斷;任何讀得到目的資料列與欄位的人,都讀得到每個被複製的檔案。沒有 grant 時,上述規則不變。見附件複製。
顯示與回傳
blob ID 實際存在 record.data;一般紀錄回應會把它們展開成 URL 已遮蔽的附件 metadata 陣列:
{
"data": {
"發票附件": [
{
"id": "44444444-4444-4444-8444-444444444444",
"created_at": "2026-07-19 02:35:00",
"url": null,
"content_type": "application/pdf",
"filename": "invoice.pdf",
"size_bytes": 245760
}
]
}
}要取得真正的檔案內容,先呼叫權限控管的 attachments.download,再使用它回傳的 signed_url;一般紀錄中的 url: null 不可使用。signed_url 是儲存 blob URL 的舊欄位名稱;這條 route 不會做密碼學簽章或加上時效限制,且回傳 expiration_seconds: 0。
注意事項
max_count的 schema 範圍是 1–100;max_file_bytes必須大於 0 且不超過 5 GiB;allowed_mime_types為 MIME allow-list,null或空陣列代表不限。columns.create可提供附件限制,但建立後不能用 PATCH 修改。- multipart init 不指定最終欄位。只有每個 attachment 欄都宣告大小上限時,早期 size check 才取最大的
max_file_bytes;只有每個 attachment 欄都宣告非空 MIME 清單時,早期 MIME check 才取allowed_mime_types聯集。實際寫入哪個 cell 是後續紀錄請求決定的。 - 兩道 init guard 彼此獨立。某欄不限制大小時,只停用早期 size guard;某欄不限制 MIME 時,只停用早期 MIME guard,因為某一欄會拒絕的檔案對另一欄可能合法。不要把「上傳成功」當成「目標欄驗證通過」。
- 真正的強制執行發生在後面兩個點。multipart complete 會重新量測組裝後的物件,並再次比對整張表最大的上限:違規時物件會被刪除、session 鍵會被清掉,blob 根本不會建立。接著每一條建立與更新紀錄的路徑都會套用目標欄位自己的 MIME 與大小限制;被拒絕時訊息會用內部
col_<hex>鍵指出欄位。 - 量測到的大小會保存下來,並出現在每個附件項目的
size_bytes。null代表大小從未被量測過——可能是舊 blob,也可能是上傳完成時大小檢查無法執行;之後寫入宣告了max_file_bytes的欄位時會補量並回填,寫入沒有大小上限的欄位則不會量測。如果物件完全讀不到,該次寫入會放行,而不是擋下來。 attachment不可有default_value、max_length或options:columns.create與tables.create會以422拒絕其中任何一項,之後透過columns.update送出default_value或options則是400。blob 必須先上傳、屬於該表解析出的公司租戶,並通過 cell 來源閘門。- 更新多檔 cell 可用
add/remove差量;新增與移除清單不可重疊。建立紀錄時請一律送完整陣列。 - 附件欄只支援是否有值的篩選:
stored_filters可用is_empty與is_not_empty。不能依 blob ID、檔名、MIME 型別或大小篩選,也永遠不能排序。 - 讀取時的陣列會略掉任何已無法解析成該表租戶 blob 的已存 ID,因此可能比儲存的 cell 短。
試試看
先從 attachments.multipartInit 開始完整上傳流程,再到 API Playground 將回傳的 blob ID 寫進紀錄。