Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/crates/application/src/plugins/ports/plugin_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ pub trait PluginRepository: Send + Sync {
patch: &JsonValue,
) -> PortResult<Option<PluginRecord>>;

async fn append_record_array_item(
&self,
record_id: Uuid,
field: &str,
item: &JsonValue,
patch: &JsonValue,
) -> PortResult<Option<PluginRecord>>;

async fn delete_record(&self, record_id: Uuid) -> PortResult<bool>;

async fn get_record(&self, record_id: Uuid) -> PortResult<Option<PluginRecord>>;
Expand Down
62 changes: 62 additions & 0 deletions api/crates/application/src/plugins/use_cases/exec_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,68 @@ where
.map_err(|err| PluginEffectError::from(anyhow::Error::from(err)))?;
}
}
"appendRecordArrayItem" => {
policy::ensure_plugin_permission(
permissions,
policy::PLUGIN_PERMISSION_DOC_WRITE,
)?;
policy::ensure_workspace_can_edit_documents(workspace_permissions)?;
let Some(record_id) = effect
.get("recordId")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
else {
continue;
};
let Some(field) = effect
.get("field")
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|value| !value.is_empty() && value.len() <= 128)
else {
continue;
};
let Some(rec) = self
.plugin_repo
.get_record(record_id)
.await
.map_err(|err| PluginEffectError::from(anyhow::Error::from(err)))?
else {
continue;
};
policy::ensure_record_owned_by_plugin(&rec.plugin, plugin)?;
if rec.scope != PluginRecordScope::Doc {
continue;
}
self.validate_doc_scope(
workspace_id,
Some(rec.scope_id),
allowed_doc_id,
doc_id_created,
actor,
true,
)
.await?;
let item = effect
.get("item")
.cloned()
.unwrap_or(serde_json::Value::Null);
let mut patch = effect
.get("patch")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
if !patch.is_object() {
patch = serde_json::json!({});
}
if let Some(obj) = patch.as_object_mut() {
obj.remove(field);
}
let _ = self
.plugin_repo
.append_record_array_item(record_id, field, &item, &patch)
.await
.map_err(|err| PluginEffectError::from(anyhow::Error::from(err)))?;
}
"deleteRecord" => {
policy::ensure_plugin_permission(
permissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,59 @@ impl PluginRepository for SqlxPluginRepository {
out.map_err(Into::into)
}

async fn append_record_array_item(
&self,
record_id: Uuid,
field: &str,
item: &JsonValue,
patch: &JsonValue,
) -> PortResult<Option<PluginRecord>> {
let out: anyhow::Result<Option<PluginRecord>> = async {
let row = sqlx::query(
r#"UPDATE plugin_records
SET data = jsonb_set(
data || $4::jsonb,
ARRAY[$2],
COALESCE(
CASE
WHEN jsonb_typeof(data -> $2) = 'array' THEN data -> $2
ELSE '[]'::jsonb
END,
'[]'::jsonb
) || jsonb_build_array($3::jsonb),
true
),
updated_at = now()
WHERE id = $1
RETURNING id, plugin, scope, scope_id, kind, data, created_at, updated_at"#,
)
.bind(record_id)
.bind(field)
.bind(item)
.bind(patch)
.fetch_optional(&self.pool)
.await?;
row.map(|r| {
let scope_raw: String = r.get("scope");
let scope = PluginRecordScope::parse(&scope_raw)
.ok_or_else(|| anyhow::anyhow!("invalid_plugin_record_scope"))?;
Ok(PluginRecord {
id: r.get("id"),
plugin: r.get("plugin"),
scope,
scope_id: r.get("scope_id"),
kind: r.get("kind"),
data: r.get("data"),
created_at: r.get("created_at"),
updated_at: r.get("updated_at"),
})
})
.transpose()
}
.await;
out.map_err(Into::into)
}

async fn delete_record(&self, record_id: Uuid) -> PortResult<bool> {
let out: anyhow::Result<bool> = async {
let res = sqlx::query("DELETE FROM plugin_records WHERE id = $1")
Expand Down
27 changes: 22 additions & 5 deletions app/src/entities/plugin/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
import type { ManifestItem as ClientManifestItem } from '@/shared/api/client'

export type PluginManifestItem = ClientManifestItem
export type PluginRecordListOptions = {
limit?: number | null
offset?: number | null
}

export const pluginKeys = {
manifest: () => ['plugins', 'manifest'] as const,
Expand Down Expand Up @@ -59,9 +63,20 @@ export async function listPluginRecords(
pluginId: string,
docId: string,
kind: string,
optionsOrToken?: PluginRecordListOptions | string,
token?: string,
) {
return withShareAuthorization(token, () => apiListRecords({ plugin: pluginId, docId, kind }))
const options = typeof optionsOrToken === 'string' ? undefined : optionsOrToken
const authToken = typeof optionsOrToken === 'string' ? optionsOrToken : token
return withShareAuthorization(authToken, () =>
apiListRecords({
plugin: pluginId,
docId,
kind,
limit: options?.limit ?? undefined,
offset: options?.offset ?? undefined,
}),
)
}

export async function createPluginRecord(
Expand All @@ -76,12 +91,14 @@ export async function createPluginRecord(
)
}

export async function updatePluginRecord(pluginId: string, id: string, patch: unknown) {
return apiPluginsUpdateRecord({ plugin: pluginId, id, requestBody: { patch } })
export async function updatePluginRecord(pluginId: string, id: string, patch: unknown, token?: string) {
return withShareAuthorization(token, () =>
apiPluginsUpdateRecord({ plugin: pluginId, id, requestBody: { patch } }),
)
}

export async function deletePluginRecord(pluginId: string, id: string) {
return apiPluginsDeleteRecord({ plugin: pluginId, id })
export async function deletePluginRecord(pluginId: string, id: string, token?: string) {
return withShareAuthorization(token, () => apiPluginsDeleteRecord({ plugin: pluginId, id }))
}

export async function getPluginKv(
Expand Down
Loading
Loading