指南:Storage 集合、KV 與加密 Settings 的完整實(shí)戰(zhàn))
CMS后端前端插件系統(tǒng)【免費(fèi)下載鏈接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress項(xiàng)目地址https://gitcode.com/gh_mirrors/emdas/emdash點(diǎn)擊查看免費(fèi)下載在 EmDash 中沙箱插件Sandboxed Plugin并非直接訪問宿主數(shù)據(jù)庫而是通過三個(gè)插件作用域plugin-scoped的數(shù)據(jù) API 讀寫數(shù)據(jù)可查詢的ctx.storage.collection記錄集合、面向用戶的ctx.settings配置支持加密密鑰、以及用于游標(biāo)與緩存的ctx.kv。本文以倉庫中的官方插件開發(fā)參考文檔 storage.md 為骨架結(jié)合 EmDash 核心源碼types.ts、storage-query.ts與插件清單 Schemaemdash-plugin.schema.json系統(tǒng)講解集合聲明、CRUD 與批量寫入、基于修訂號(hào)的樂觀并發(fā)控制CAS、謂詞守衛(wèi)原子更新updateIf、索引查詢分頁以及 KV 與加密設(shè)置的使用邊界幫助你寫出既安全又可在三種運(yùn)行時(shí)native、Cloudflare 沙箱、Node/workerd 沙箱間可移植的存儲(chǔ)代碼。三大插件級(jí)數(shù)據(jù) API 總覽沙箱插件可以使用的數(shù)據(jù) API 只有三個(gè)全部走宿主數(shù)據(jù)庫并按運(yùn)行時(shí)插件 IDruntime plugin ID做隔離且不需要聲明任何 capability參見 SKILL.md 中的 capability 表格說明Settings, KV, declared storage, logging, and cron scheduling are plugin-scoped and need no capabilityAPI用途ctx.storage.collection在emdash-plugin.jsonc中聲明的、可查詢的記錄集合ctx.settings用戶可配置的設(shè)置項(xiàng)支持加密密鑰secretctx.kv游標(biāo)cursors、緩存值及其他鍵值狀態(tài)需要特別強(qiáng)調(diào)的是插件作用域 運(yùn)行時(shí)隔離每個(gè)插件只能看到自己名下的集合與鍵無法越界訪問其他插件或宿主的數(shù)據(jù)。三個(gè)存儲(chǔ)都落在宿主數(shù)據(jù)庫中因此不需要為它們申請(qǐng)capabilities也正因?yàn)槿绱舜鎯?chǔ) API 是所有沙箱插件默認(rèn)就有的能力。在清單中聲明存儲(chǔ)集合任何集合與查詢索引都必須先在插件清單emdash-plugin.jsonc中聲明。清單的storage字段在 emdash-plugin.schema.json 中有完整的 JSON Schema 約束{ storage: { submissions: { indexes: [formId, status, createdAt, [formId, createdAt]], uniqueIndexes: [externalId], }, }, }聲明規(guī)則與 Schema 約束集合名必須匹配^[a-z][a-z0-9_]*$小寫字母開頭可含小寫字母、數(shù)字、下劃線運(yùn)行時(shí)按插件命名空間隔離。indexes是必填字段Schema 中集合對(duì)象required: [indexes]每個(gè)索引要么是單個(gè)字段名字符串要么是復(fù)合索引字段名數(shù)組。uniqueIndexes聲明唯一索引其中的字段本身已經(jīng)可查詢不要再重復(fù)寫進(jìn)indexes。未聲明的集合會(huì)被沙箱橋sandbox bridge直接拒絕An undeclared collection is rejected by the sandbox bridge因此清單是存儲(chǔ)可用性的硬約束而不是可選優(yōu)化。從源碼實(shí)現(xiàn)看這些聲明會(huì)被編譯為實(shí)際的查詢約束getIndexedFields()storage-query.ts把聲明拍平成可索引字段集合validateWhereClause()隨后校驗(yàn)where里的每個(gè)字段是否都在該集合內(nèi)——這正是只能過濾/排序已聲明索引字段這一規(guī)則在底層的強(qiáng)制實(shí)施。集合操作一套可移植的 API每個(gè)已聲明集合都暴露統(tǒng)一的StorageCollectionT接口在 native、Cloudflare 沙箱和 Node/workerd 沙箱三種執(zhí)行方式下保持一致的語義完整定義見 types.tsinterface StorageCollectionT unknown { get(id: string): PromiseT | null; put(id: string, data: T): Promisevoid; delete(id: string): Promiseboolean; exists(id: string): Promiseboolean; getVersioned(id: string): Promise{ value: T; revision: string } | null; compareAndSet( id: string, expectedRevision: string | null, data: T, ): Promise{ applied: true; revision: string } | { applied: false }; compareAndDelete(id: string, expectedRevision: string): Promise{ applied: boolean }; updateIf(id: string, args: UpdateIfArgsT): PromiseUpdateIfResultT; getMany(ids: string[]): PromiseMapstring, T; putMany(items: Array{ id: string; data: T }): Promisevoid; deleteMany(ids: string[]): Promisenumber; query(options?: QueryOptions): Promise{ items: Array{ id: string; data: T }; cursor?: string; hasMore: boolean; }; count(where?: WhereClause): Promisenumber; }幾點(diǎn)實(shí)現(xiàn)細(xì)節(jié)值得注意不依賴異步迭代器源碼注釋明確 No async iterators - all operations return promises with pagination即查詢一律走分頁結(jié)果而非流式迭代。批量方法可跨橋getMany()返回Mapstring, T即使跨過沙箱橋Cloudflare 或 Node/workerd 的 bridge也保持Map類型。內(nèi)容批量方法不在StorageCollection中Node/workerd wrapper 還額外含有 content batch 方法但它們不屬于本接口上面這組 storage 批量方法才是兩種 runner 之間可移植的公共子集?;A(chǔ)與批量寫入最基本的 CRUD 與批量操作如下以表單插件收集的提交記錄為例倉庫中 forms 插件 即使用這類集合處理提交數(shù)據(jù)const submissions ctx.storage.submissions as StorageCollectionSubmission; await submissions.put(sub_123, { formId: contact, status: pending, createdAt: new Date().toISOString(), }); const item await submissions.get(sub_123); const exists await submissions.exists(sub_123); const items await submissions.getMany([sub_123, sub_456]); await submissions.putMany([ { id: sub_456, data: { formId: contact, status: pending } }, { id: sub_789, data: { formId: sales, status: pending } }, ]); const deleted await submissions.deleteMany([sub_456, sub_789]);使用要點(diǎn)put(id, data)全量替換該 id 下的 JSON 文檔delete(id)返回布爾值表示是否確實(shí)刪除了記錄。getMany對(duì)不存在的 id 會(huì)自然缺項(xiàng)返回的Map中無該鍵適合做批量預(yù)取。deleteMany返回實(shí)際刪除的數(shù)量。批量操作與單條操作共享同一套可移植語義跨沙箱橋行為一致?;谛抻喬?hào)的樂觀并發(fā)CAS當(dāng)多個(gè)并發(fā)請(qǐng)求可能替換同一個(gè)完整值時(shí)不要用讀-改-寫裸奔而應(yīng)使用getVersioned()、compareAndSet()、compareAndDelete()三個(gè)基于修訂號(hào)revision的操作const current await submissions.getVersioned(sub_123); if (!current) throw new Error(Submission not found); const result await submissions.compareAndSet(sub_123, current.revision, { ...current.value, status: processing, }); if (!result.applied) { // 另一個(gè)請(qǐng)求已經(jīng)修改或刪除了該值。重新讀取后再重試。 }各操作的前置條件preconditions總結(jié)操作行為getVersioned(key)返回{ value, revision }只有記錄不存在時(shí)才返回nullcompareAndSet(key, null, value)僅當(dāng)記錄不存在時(shí)創(chuàng)建create-if-absentcompareAndSet(key, revision, value)僅當(dāng)當(dāng)前修訂號(hào)匹配時(shí)替換compareAndDelete(key, revision)僅當(dāng)當(dāng)前修訂號(hào)匹配時(shí)刪除語義邊界源碼注釋與文檔一致存儲(chǔ)的 JSONnull仍然返回版本化信封getVersioned對(duì)存在但值為 null返回{ value: null, revision }只有整條記錄缺失才返回null。每次成功寫入都會(huì)改變修訂號(hào)包括寫入相同值的put()/set()修訂號(hào)是不透明的、按 key 隔離的值必須原樣回傳不能解析或構(gòu)造。沖突返回applied: false而非法輸入、權(quán)限失敗、唯一索引沖突、數(shù)據(jù)庫錯(cuò)誤等會(huì)直接 reject。CAS 不是外部副作用的 exactly-once 機(jī)制沖突后要重新讀取、重新計(jì)算并把重試次數(shù)控制在有界范圍內(nèi)丟失響應(yīng)可能讓寫入結(jié)果未知因此不要把 CAS 當(dāng)作外部副作用如發(fā)郵件、扣款的冪等保證。版本化方法在ctx.kv上同樣可用典型用途是無鎖計(jì)數(shù)器const current await ctx.kv.getVersionednumber(state:completed); const next (current?.value ?? 0) 1; const result await ctx.kv.compareAndSet(state:completed, current?.revision ?? null, next);謂詞守衛(wèi)原子更新updateIfupdateIf()在存儲(chǔ)數(shù)據(jù)匹配守衛(wèi)guard時(shí)修改已存在文檔的字段。守衛(wèi)、字段替換和整數(shù)增量在單條記錄上原子執(zhí)行——這正是源碼注釋中強(qiáng)調(diào)的 no-oversell 原語守衛(wèi)與算術(shù)位于同一條UPDATE … RETURNING語句中N 個(gè)并發(fā)守衛(wèi)遞減會(huì)正確串行化見 types.ts 的updateIf文檔。const result await submissions.updateIf(sub_123, { where: { status: pending, attempts: { lt: 3 } }, set: { status: processing, lastAttemptAt: new Date().toISOString() }, delta: { attempts: { inc: 1 } }, }); if (result.applied) { ctx.log.info(Claimed submission, { submission: result.data }); }返回{ applied: false }的四種情形行不存在、守衛(wèi)不匹配、存儲(chǔ)文檔不是對(duì)象、整數(shù)算術(shù)不安全。updateIf是純更新操作永遠(yuǎn)不會(huì)插入缺失行源碼注釋applied: falseintentionally conflates row absent and guard failed兩者有意不區(qū)分。UpdateIfArgs的真實(shí)定義types.ts與使用規(guī)則where必填顯式{}表示匹配任意存在的行等同 update-if-row-exists源碼提醒這在防超賣場景下是個(gè) footgun應(yīng)寫真實(shí)謂詞如{ stock: { gte: 1 } }。where復(fù)用QueryOptions的WhereClause在 SQL 內(nèi)求值與query()使用同一套數(shù)值正確、全序比較語義。set替換傳入的頂層字段未涉及的字段保持不變。delta中每個(gè)字段恰好一個(gè)安全整數(shù)inc或dec缺失或null的計(jì)數(shù)器從零開始COALESCE(base, 0) ± n。同一字段不能同時(shí)出現(xiàn)在set和delta中set/delta至少留一個(gè)已定義字段。需要保持非負(fù)時(shí)把dec: n與gte: n守衛(wèi)配對(duì)。set與delta是獨(dú)立參數(shù)而非聯(lián)合類型因此一個(gè)恰好長得像{ inc: 5 }的整體值永遠(yuǎn)不會(huì)被誤判為增量源碼注釋明確說明這一設(shè)計(jì)動(dòng)機(jī)。錯(cuò)誤處理畸形參數(shù)會(huì)在不寫入的情況下 reject。在原生 PostgreSQL 執(zhí)行中序列化失敗與死鎖會(huì)拋出StorageSerializationErrorstorage-query.ts其code STORAGE_SERIALIZATION_FAILURE、retryable true并攜帶可選的sqlStatePostgres SQLSTATE如40001/40P01。沙箱傳輸會(huì)保留code與retryable等安全字段但不保證instanceof——跨橋判斷時(shí)請(qǐng)檢查code和retryable字段而非類型。重試整個(gè)顯式事務(wù)explicit transaction前要先整體重啟該事務(wù)。索引查詢與分頁查詢只能過濾或排序已聲明索引的字段。query()返回分頁結(jié)果count(where)接受同樣的索引過濾條件const result await submissions.query({ where: { formId: contact, status: { in: [pending, processing] }, createdAt: { gte: 2026-01-01 }, }, orderBy: { createdAt: desc }, limit: 100, cursor, });支持的過濾形式精確值{ field: contact }枚舉{ in: [...] }前綴{ startsWith: ... }底層用 LIKE 實(shí)現(xiàn)escapeLikePattern()會(huì)轉(zhuǎn)義%、_、\等通配符見 storage-query.ts范圍對(duì)象使用gt、gte、lt、lte至少需要一個(gè)有定義的邊界分頁約定query()默認(rèn)返回 50 條每頁最多 100 條。用cursor翻頁直到hasMore為false。復(fù)合索引的字段順序決定可用的查詢形態(tài)[formId, createdAt]支持按formId過濾 按createdAt排序但它不能替代獨(dú)立的createdAt索引——省略formId的查詢用不到該復(fù)合索引。KV 操作KV 支持無條件讀寫、版本化讀寫、刪除與前綴列舉interface KVAccess { getT(key: string): PromiseT | null; set(key: string, value: unknown): Promisevoid; delete(key: string): Promiseboolean; list(prefix?: string): PromiseArray{ key: string; value: unknown }; getVersionedT(key: string): Promise{ value: T; revision: string } | null; compareAndSet( key: string, expectedRevision: string | null, value: unknown, ): Promise{ applied: true; revision: string } | { applied: false }; compareAndDelete(key: string, expectedRevision: string): Promise{ applied: boolean }; }源碼注釋types.ts給出了官方鍵命名約定state:*插件內(nèi)部狀態(tài)不對(duì)用戶展示如游標(biāo)、計(jì)數(shù)器cache:*可復(fù)用的計(jì)算結(jié)果或遠(yuǎn)程數(shù)據(jù)settings:*EmDash 0.x 期間的兼容別名見下文。用穩(wěn)定的前綴保持內(nèi)部 KV 鍵可發(fā)現(xiàn)、可管理await ctx.settings.set(webhookUrl, url); await ctx.kv.set(state:lastRun, new Date().toISOString()); await ctx.kv.set(cache:summary, summary); const settings await ctx.settings.list();設(shè)置Settings與密鑰加密ctx.settings是面向用戶的配置入口與后臺(tái)管理界面admin的生成表單直接打通插件 CLI 會(huì)序列化admin.settingsSchema兩個(gè)沙箱橋把ctx.settings路由到與后臺(tái)表單相同的 options 記錄上——用戶在管理表單里保存的值通過ctx.settings.get(key)即可讀到。完整設(shè)置 API 支持set、delete、list、getVersioned、compareAndSet、compareAndDelete。settingsSchema的字段類型在 emdash-plugin.schema.json 中定義包括string可帶multiline、default、number可帶min/max、boolean、selectoptions數(shù)組、url、email以及加密類型secret。secret 字段的加密機(jī)制聲明為secret的字段使用帶版本的 AES-GCM 信封插件 ID 與設(shè)置鍵作為認(rèn)證數(shù)據(jù)authenticated data參與加密防止密文被替換到其他插件/其他鍵上。EMDASH_ENCRYPTION_KEY環(huán)境變量可包含逗號(hào)分隔的密鑰輪換列表第一個(gè)密鑰用于加密新值信封中的kid選擇用于讀取的密鑰。缺失、錯(cuò)誤或被篡改的密鑰會(huì)安全失敗fail closed不會(huì)暴露明文。已存在的明文 secret 仍可讀取并在再次保存時(shí)轉(zhuǎn)為加密存儲(chǔ)。運(yùn)維注意必須把完整的密鑰列表與運(yùn)營備份放在一起如果恢復(fù)數(shù)據(jù)庫時(shí)缺少其加密設(shè)置引用的任一密鑰這些設(shè)置值將無法讀取。另外ctx.kv.get(settings:key)在整個(gè) EmDash 0.x 中仍是兼容別名但新插件應(yīng)統(tǒng)一使用ctx.settings。結(jié)語EmDash 的插件存儲(chǔ)體系用清單聲明 統(tǒng)一接口 沙箱橋隔離三條原則把插件數(shù)據(jù)訪問收斂成可控、可移植、可審計(jì)的形態(tài)集合索引必須顯式聲明并由橋強(qiáng)制校驗(yàn)并發(fā)安全交給修訂號(hào) CAS 與謂詞守衛(wèi)updateIf單語句原子更新用戶配置與密鑰安全由ctx.settings和 AES-GCM 信封統(tǒng)一承擔(dān)。編寫插件時(shí)優(yōu)先對(duì)照 storage.md 與 SKILL.md 的 capability 說明以 types.ts 的導(dǎo)出類型為準(zhǔn)即可寫出在 native、Cloudflare 與 Node/workerd 三種運(yùn)行時(shí)之間行為一致、無需額外 capability 的持久化代碼。贊分享CMS后端前端插件系統(tǒng)【免費(fèi)下載鏈接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress項(xiàng)目地址https://gitcode.com/gh_mirrors/emdas/emdash點(diǎn)擊查看免費(fèi)下載相關(guān)推薦EmDash 插件存儲(chǔ)指南Storage、Settings 與 KV 的聲明、讀寫與并發(fā)控制EmDash 插件存儲(chǔ)指南Storage、Settings 與 KV 的聲明、讀寫與并發(fā)控制 EmDash 為沙箱化插件提供了三套插件級(jí)數(shù)據(jù) API可查詢的CMS后端前端插件系統(tǒng)EmDash 插件存儲(chǔ)指南基于 Sandboxed 插件的 Collection、Settings 與 KV 數(shù)據(jù) API 實(shí)戰(zhàn)EmDash 插件存儲(chǔ)指南基于 Sandboxed 插件的 Collection、Settings 與 KV 數(shù)據(jù) API 實(shí)戰(zhàn) 導(dǎo)讀 本文是 EmDashCMS后端前端插件系統(tǒng)EmDash 插件存儲(chǔ)與 KV 完全指南ctx.storage、ctx.settings 與 ctx.kv 的聲明、并發(fā)控制與加密實(shí)踐EmDash 插件存儲(chǔ)與 KV 完全指南ctx.storage、ctx.settings 與 ctx.kv 的聲明、并發(fā)控制與加密實(shí)踐 沙盒化插件SandbCMS后端前端插件系統(tǒng)創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考