diff --git a/packages/plugin-runtime-types/src/bindings/gen_events.ts b/packages/plugin-runtime-types/src/bindings/gen_events.ts index 173f5cbe..23411ae7 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_events.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_events.ts @@ -361,7 +361,11 @@ export type GetKeyValueRequest = { key: string, }; export type GetKeyValueResponse = { value?: string, }; -export type GetTemplateFunctionsResponse = { functions: Array, pluginRefId: string, }; +export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, }; + +export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, }; + +export type GetTemplateFunctionSummaryResponse = { functions: Array, pluginRefId: string, }; export type GetThemesRequest = Record; @@ -385,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, }; export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; -export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; +export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; export type JsonPrimitive = string | number | boolean | null; diff --git a/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts b/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts index 454da2cc..7dabdb1c 100644 --- a/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts @@ -1,12 +1,21 @@ import { CallTemplateFunctionArgs, + FormInput, + GetHttpAuthenticationConfigRequest, TemplateFunction, -} from "../bindings/gen_events"; -import { Context } from "./Context"; + TemplateFunctionArg, +} from '../bindings/gen_events'; +import { MaybePromise } from '../helpers'; +import { Context } from './Context'; + +export type DynamicTemplateFunctionArg = FormInput & { + dynamic( + ctx: Context, + args: GetHttpAuthenticationConfigRequest, + ): MaybePromise | undefined | null>; +}; export type TemplateFunctionPlugin = TemplateFunction & { - onRender( - ctx: Context, - args: CallTemplateFunctionArgs, - ): Promise; + args: (TemplateFunctionArg | DynamicTemplateFunctionArg)[]; + onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise; }; diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index 1f902a20..25f07132 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -186,20 +186,55 @@ export class PluginInstance { } if ( - payload.type === 'get_template_functions_request' && + payload.type === 'get_template_function_summary_request' && Array.isArray(this.#mod?.templateFunctions) ) { - const reply: TemplateFunction[] = this.#mod.templateFunctions.map((templateFunction) => { - return { - ...migrateTemplateFunctionSelectOptions(templateFunction), - // Add everything except render - onRender: undefined, - }; - }); + const functions: TemplateFunction[] = this.#mod.templateFunctions.map( + (templateFunction) => { + return { + ...migrateTemplateFunctionSelectOptions(templateFunction), + // Add everything except render + onRender: undefined, + }; + }, + ); const replyPayload: InternalEventPayload = { - type: 'get_template_functions_response', + type: 'get_template_function_summary_response', pluginRefId: this.#workerData.pluginRefId, - functions: reply, + functions, + }; + this.#sendPayload(windowContext, replyPayload, replyId); + return; + } + + if ( + payload.type === 'get_template_function_config_request' && + Array.isArray(this.#mod?.templateFunctions) + ) { + let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name); + if (templateFunction == null) { + this.#sendEmpty(windowContext, replyId); + return; + } + + templateFunction = migrateTemplateFunctionSelectOptions(templateFunction); + // @ts-ignore + delete templateFunction.onRender; + const resolvedArgs: TemplateFunctionArg[] = []; + for (const arg of templateFunction.args) { + if (arg && 'dynamic' in arg) { + const dynamicAttrs = await arg.dynamic(ctx, payload); + const { dynamic, ...other } = arg; + resolvedArgs.push({ ...other, ...dynamicAttrs } as TemplateFunctionArg); + } else if (arg) { + resolvedArgs.push(arg); + } + templateFunction.args = resolvedArgs; + } + const replyPayload: InternalEventPayload = { + type: 'get_template_function_config_response', + pluginRefId: this.#workerData.pluginRefId, + function: templateFunction, }; this.#sendPayload(windowContext, replyPayload, replyId); return; diff --git a/packages/plugin-runtime/src/migrations.ts b/packages/plugin-runtime/src/migrations.ts index d64bf60b..830c6f84 100644 --- a/packages/plugin-runtime/src/migrations.ts +++ b/packages/plugin-runtime/src/migrations.ts @@ -1,6 +1,8 @@ -import { TemplateFunction } from '@yaakapp/api'; +import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin'; -export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction { +export function migrateTemplateFunctionSelectOptions( + f: TemplateFunctionPlugin, +): TemplateFunctionPlugin { const migratedArgs = f.args.map((a) => { if (a.type === 'select') { a.options = a.options.map((o) => ({ diff --git a/plugins/template-function-response/src/index.ts b/plugins/template-function-response/src/index.ts index b2153983..8b0f0eda 100644 --- a/plugins/template-function-response/src/index.ts +++ b/plugins/template-function-response/src/index.ts @@ -3,25 +3,44 @@ import type { CallTemplateFunctionArgs, Context, FormInput, + GetHttpAuthenticationConfigRequest, HttpResponse, PluginDefinition, RenderPurpose, } from '@yaakapp/api'; +import type { DynamicTemplateFunctionArg } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin'; import { JSONPath } from 'jsonpath-plus'; import { readFileSync } from 'node:fs'; import xpath from 'xpath'; +const BEHAVIOR_TTL = 'ttl'; +const BEHAVIOR_ALWAYS = 'always'; +const BEHAVIOR_SMART = 'smart'; + const behaviorArg: FormInput = { type: 'select', name: 'behavior', label: 'Sending Behavior', defaultValue: 'smart', options: [ - { label: 'When no responses', value: 'smart' }, - { label: 'Always', value: 'always' }, + { label: 'When no responses', value: BEHAVIOR_SMART }, + { label: 'Always', value: BEHAVIOR_ALWAYS }, + { label: 'When expired', value: BEHAVIOR_TTL }, ], }; +const ttlArg: DynamicTemplateFunctionArg = { + type: 'text', + name: 'ttl', + label: 'Expiration Time (seconds)', + placeholder: '0', + description: 'Resend the request when the latest response is older than this many seconds, or if there are no responses yet.', + dynamic(_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) { + const show = values.behavior === BEHAVIOR_TTL; + return { hidden: !show }; + }, +}; + const requestArg: FormInput = { type: 'http_request', name: 'request', @@ -42,6 +61,7 @@ export const plugin: PluginDefinition = { placeholder: 'Content-Type', }, behaviorArg, + ttlArg, ], async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { if (!args.values.request || !args.values.header) return null; @@ -50,6 +70,7 @@ export const plugin: PluginDefinition = { requestId: String(args.values.request || ''), purpose: args.purpose, behavior: args.values.behavior ? String(args.values.behavior) : null, + ttl: String(args.values.ttl || ''), }); if (response == null) return null; @@ -72,6 +93,7 @@ export const plugin: PluginDefinition = { placeholder: '$.books[0].id or /books[0]/id', }, behaviorArg, + ttlArg, ], async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { if (!args.values.request || !args.values.path) return null; @@ -80,6 +102,7 @@ export const plugin: PluginDefinition = { requestId: String(args.values.request || ''), purpose: args.purpose, behavior: args.values.behavior ? String(args.values.behavior) : null, + ttl: String(args.values.ttl || ''), }); if (response == null) return null; @@ -113,7 +136,7 @@ export const plugin: PluginDefinition = { name: 'response.body.raw', description: 'Access the entire response body, as text', aliases: ['response'], - args: [requestArg, behaviorArg], + args: [requestArg, behaviorArg, ttlArg], async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise { if (!args.values.request) return null; @@ -121,6 +144,7 @@ export const plugin: PluginDefinition = { requestId: String(args.values.request || ''), purpose: args.purpose, behavior: args.values.behavior ? String(args.values.behavior) : null, + ttl: String(args.values.ttl || ''), }); if (response == null) return null; @@ -177,9 +201,11 @@ async function getResponse( requestId, behavior, purpose, + ttl, }: { requestId: string; behavior: string | null; + ttl: string | null; purpose: RenderPurpose; }, ): Promise { @@ -203,7 +229,11 @@ async function getResponse( const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior; // Send if no responses and "smart," or "always" - if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') { + if ( + (finalBehavior === 'smart' && response == null) || + finalBehavior === 'always' || + (finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl)) + ) { // NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...) const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose }); response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest }); @@ -211,3 +241,12 @@ async function getResponse( return response; } + +function shouldSendExpired(response: HttpResponse | null, ttl: string | null): boolean { + if (response == null) return true; + const ttlSeconds = parseInt(ttl || '0'); + if (isNaN(ttlSeconds)) throw new Error(`Invalid TTL "${ttl}"`); + const nowMillis = Date.now(); + const respMillis = new Date(response.createdAt + 'Z').getTime(); + return respMillis + ttlSeconds * 1000 < nowMillis; +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 80e6cb97..fefcb236 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -38,13 +38,7 @@ use yaak_models::models::{ }; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; -use yaak_plugins::events::{ - CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, - CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, - GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, - GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, InternalEvent, - InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest, -}; +use yaak_plugins::events::{CallGrpcRequestActionArgs, CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, Color, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionSummaryResponse, GetTemplateFunctionConfigResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, ShowToastRequest}; use yaak_plugins::manager::PluginManager; use yaak_plugins::plugin_meta::PluginMetadata; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -827,11 +821,36 @@ async fn cmd_grpc_request_actions( } #[tauri::command] -async fn cmd_template_functions( +async fn cmd_template_function_summaries( window: WebviewWindow, plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - Ok(plugin_manager.get_template_functions(&window).await?) +) -> YaakResult> { + let results = plugin_manager.get_template_function_summaries(&window).await?; + Ok(results) +} + +#[tauri::command] +async fn cmd_template_function_config( + window: WebviewWindow, + plugin_manager: State<'_, PluginManager>, + function_name: &str, + values: HashMap, + model: AnyModel, + environment_id: Option<&str>, +) -> YaakResult { + let (workspace_id, folder_id) = match model.clone() { + AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id), + AnyModel::Folder(m) => (m.workspace_id, m.folder_id), + AnyModel::Workspace(m) => (m.id, None), + m => { + return Err(GenericError(format!("Unsupported model to call template functions {m:?}"))); + } + }; + let environment_chain = + window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; + Ok(plugin_manager.get_template_function_config(&window, function_name, environment_chain, values, model.id()).await?) } #[tauri::command] @@ -849,10 +868,10 @@ async fn cmd_get_http_authentication_config( plugin_manager: State<'_, PluginManager>, auth_name: &str, values: HashMap, - request: AnyModel, + model: AnyModel, environment_id: Option<&str>, ) -> YaakResult { - let (workspace_id, folder_id) = match request.clone() { + let (workspace_id, folder_id) = match model.clone() { AnyModel::HttpRequest(m) => (m.workspace_id, m.folder_id), AnyModel::GrpcRequest(m) => (m.workspace_id, m.folder_id), AnyModel::WebsocketRequest(m) => (m.workspace_id, m.folder_id), @@ -867,7 +886,7 @@ async fn cmd_get_http_authentication_config( window.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; Ok(plugin_manager - .get_http_authentication_config(&window, environment_chain, auth_name, values, request.id()) + .get_http_authentication_config(&window, environment_chain, auth_name, values, model.id()) .await?) } @@ -1416,7 +1435,8 @@ pub fn run() { cmd_send_ephemeral_request, cmd_send_http_request, cmd_send_folder, - cmd_template_functions, + cmd_template_function_config, + cmd_template_function_summaries, cmd_template_tokens_to_string, // // diff --git a/src-tauri/yaak-plugins/bindings/gen_events.ts b/src-tauri/yaak-plugins/bindings/gen_events.ts index 173f5cbe..23411ae7 100644 --- a/src-tauri/yaak-plugins/bindings/gen_events.ts +++ b/src-tauri/yaak-plugins/bindings/gen_events.ts @@ -361,7 +361,11 @@ export type GetKeyValueRequest = { key: string, }; export type GetKeyValueResponse = { value?: string, }; -export type GetTemplateFunctionsResponse = { functions: Array, pluginRefId: string, }; +export type GetTemplateFunctionConfigRequest = { contextId: string, name: string, values: { [key in string]?: JsonPrimitive }, }; + +export type GetTemplateFunctionConfigResponse = { function: TemplateFunction, pluginRefId: string, }; + +export type GetTemplateFunctionSummaryResponse = { functions: Array, pluginRefId: string, }; export type GetThemesRequest = Record; @@ -385,7 +389,7 @@ export type ImportResponse = { resources: ImportResources, }; export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; -export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; +export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } | { "type": "reload_response" } & ReloadResponse | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "list_cookie_names_request" } & ListCookieNamesRequest | { "type": "list_cookie_names_response" } & ListCookieNamesResponse | { "type": "get_cookie_value_request" } & GetCookieValueRequest | { "type": "get_cookie_value_response" } & GetCookieValueResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_grpc_request_actions_request" } & EmptyPayload | { "type": "get_grpc_request_actions_response" } & GetGrpcRequestActionsResponse | { "type": "call_grpc_request_action_request" } & CallGrpcRequestActionRequest | { "type": "get_template_function_summary_request" } & EmptyPayload | { "type": "get_template_function_summary_response" } & GetTemplateFunctionSummaryResponse | { "type": "get_template_function_config_request" } & GetTemplateFunctionConfigRequest | { "type": "get_template_function_config_response" } & GetTemplateFunctionConfigResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "render_grpc_request_request" } & RenderGrpcRequestRequest | { "type": "render_grpc_request_response" } & RenderGrpcRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "window_close_event" } | { "type": "close_window_request" } & CloseWindowRequest | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "get_themes_request" } & GetThemesRequest | { "type": "get_themes_response" } & GetThemesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse; export type JsonPrimitive = string | number | boolean | null; diff --git a/src-tauri/yaak-plugins/src/events.rs b/src-tauri/yaak-plugins/src/events.rs index 4437c3bf..6cd2c7ae 100644 --- a/src-tauri/yaak-plugins/src/events.rs +++ b/src-tauri/yaak-plugins/src/events.rs @@ -99,8 +99,10 @@ pub enum InternalEventPayload { CallGrpcRequestActionRequest(CallGrpcRequestActionRequest), // Template Functions - GetTemplateFunctionsRequest, - GetTemplateFunctionsResponse(GetTemplateFunctionsResponse), + GetTemplateFunctionSummaryRequest(EmptyPayload), + GetTemplateFunctionSummaryResponse(GetTemplateFunctionSummaryResponse), + GetTemplateFunctionConfigRequest(GetTemplateFunctionConfigRequest), + GetTemplateFunctionConfigResponse(GetTemplateFunctionConfigResponse), CallTemplateFunctionRequest(CallTemplateFunctionRequest), CallTemplateFunctionResponse(CallTemplateFunctionResponse), @@ -673,11 +675,28 @@ pub struct CallHttpAuthenticationResponse { #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export, export_to = "gen_events.ts")] -pub struct GetTemplateFunctionsResponse { +pub struct GetTemplateFunctionSummaryResponse { pub functions: Vec, pub plugin_ref_id: String, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "gen_events.ts")] +pub struct GetTemplateFunctionConfigRequest { + pub context_id: String, + pub name: String, + pub values: HashMap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "gen_events.ts")] +pub struct GetTemplateFunctionConfigResponse { + pub function: TemplateFunction, + pub plugin_ref_id: String, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export, export_to = "gen_events.ts")] diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 305dcf62..c6ba05f9 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -10,7 +10,8 @@ use crate::events::{ FilterRequest, FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigRequest, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, - GetTemplateFunctionsResponse, GetThemesRequest, GetThemesResponse, ImportRequest, + GetTemplateFunctionConfigRequest, GetTemplateFunctionConfigResponse, + GetTemplateFunctionSummaryResponse, GetThemesRequest, GetThemesResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, JsonPrimitive, PluginWindowContext, RenderPurpose, }; @@ -489,35 +490,59 @@ impl PluginManager { Ok(all_actions) } - pub async fn get_template_functions( + pub async fn get_template_function_config( &self, window: &WebviewWindow, - ) -> Result> { - self.get_template_functions_with_context(&PluginWindowContext::new(&window)).await - } + fn_name: &str, + environment_chain: Vec, + values: HashMap, + model_id: &str, + ) -> Result { + let results = self.get_template_function_summaries(window).await?; + let r = results + .iter() + .find(|r| r.functions.iter().any(|f| f.name == fn_name)) + .ok_or_else(|| PluginNotFoundErr(fn_name.into()))?; + let plugin = self + .get_plugin_by_ref_id(&r.plugin_ref_id) + .await + .ok_or_else(|| PluginNotFoundErr(r.plugin_ref_id.clone()))?; - pub async fn get_template_functions_with_context( - &self, - window_context: &PluginWindowContext, - ) -> Result> { - let reply_events = self - .send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest) + let window_context = &PluginWindowContext::new(&window); + let vars = &make_vars_hashmap(environment_chain); + let cb = PluginTemplateCallback::new( + window.app_handle(), + &window_context, + RenderPurpose::Preview, + ); + // We don't want to fail for this op because the UI will not be able to list any auth types then + let render_opt = RenderOptions { + error_behavior: RenderErrorBehavior::ReturnEmpty, + }; + let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; + let context_id = format!("{:x}", md5::compute(model_id.to_string())); + + let event = self + .send_to_plugin_and_wait( + &PluginWindowContext::new(window), + &plugin, + &InternalEventPayload::GetTemplateFunctionConfigRequest( + GetTemplateFunctionConfigRequest { + values: serde_json::from_value(rendered_values)?, + name: fn_name.to_string(), + context_id, + }, + ), + ) .await?; - - let mut result = Vec::new(); - for event in reply_events { - if let InternalEventPayload::GetTemplateFunctionsResponse(resp) = event.payload { - result.push(resp.clone()); + match event.payload { + InternalEventPayload::GetTemplateFunctionConfigResponse(resp) => Ok(resp), + InternalEventPayload::EmptyResponse(_) => { + Err(PluginErr("Template function plugin returned empty".to_string())) } + InternalEventPayload::ErrorResponse(e) => Err(PluginErr(e.error)), + e => Err(PluginErr(format!("Template function plugin returned invalid event {:?}", e))), } - - // Add Rust-based functions - result.push(GetTemplateFunctionsResponse { - plugin_ref_id: "__NATIVE__".to_string(), // Meh - functions: vec![template_function_secure(), template_function_keyring()], - }); - - Ok(result) } pub async fn call_http_request_action( @@ -587,7 +612,7 @@ impl PluginManager { environment_chain: Vec, auth_name: &str, values: HashMap, - request_id: &str, + model_id: &str, ) -> Result { let results = self.get_http_authentication_summaries(window).await?; let plugin = results @@ -606,7 +631,7 @@ impl PluginManager { error_behavior: RenderErrorBehavior::ReturnEmpty, }; let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; - let context_id = format!("{:x}", md5::compute(request_id.to_string())); + let context_id = format!("{:x}", md5::compute(model_id.to_string())); let event = self .send_to_plugin_and_wait( &PluginWindowContext::new(window), @@ -720,6 +745,34 @@ impl PluginManager { } } + pub async fn get_template_function_summaries( + &self, + window: &WebviewWindow, + ) -> Result> { + let window_context = PluginWindowContext::new(window); + let reply_events = self + .send_and_wait( + &window_context, + &InternalEventPayload::GetTemplateFunctionSummaryRequest(EmptyPayload {}), + ) + .await?; + + let mut results = Vec::new(); + for event in reply_events { + if let InternalEventPayload::GetTemplateFunctionSummaryResponse(resp) = event.payload { + results.push(resp.clone()); + } + } + + // Add Rust-based functions + results.push(GetTemplateFunctionSummaryResponse { + plugin_ref_id: "__NATIVE__".to_string(), // Meh + functions: vec![template_function_secure(), template_function_keyring()], + }); + + Ok(results) + } + pub async fn call_template_function( &self, window_context: &PluginWindowContext, diff --git a/src-web/components/TemplateFunctionDialog.tsx b/src-web/components/TemplateFunctionDialog.tsx index ca270def..98005289 100644 --- a/src-web/components/TemplateFunctionDialog.tsx +++ b/src-web/components/TemplateFunctionDialog.tsx @@ -1,9 +1,17 @@ +import type { + Folder, + GrpcRequest, + HttpRequest, + WebsocketRequest, + Workspace, +} from '@yaakapp-internal/models'; import type { TemplateFunction } from '@yaakapp-internal/plugins'; import type { FnArg, Tokens } from '@yaakapp-internal/templates'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import { useDebouncedValue } from '../hooks/useDebouncedValue'; import { useRenderTemplate } from '../hooks/useRenderTemplate'; +import { useTemplateFunctionConfig } from '../hooks/useTemplateFunctionConfig'; import { templateTokensToString, useTemplateTokensToString, @@ -24,6 +32,7 @@ interface Props { initialTokens: Tokens; hide: () => void; onChange: (insert: string) => void; + model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace; } export function TemplateFunctionDialog({ initialTokens, templateFunction, ...props }: Props) { @@ -84,14 +93,15 @@ export function TemplateFunctionDialog({ initialTokens, templateFunction, ...pro } function InitializedTemplateFunctionDialog({ - templateFunction, - hide, + templateFunction: { name }, initialArgValues, + hide, onChange, + model, }: Omit & { initialArgValues: Record; }) { - const enablePreview = templateFunction.name !== 'secure'; + const enablePreview = name !== 'secure'; const [showSecretsInPreview, toggleShowSecretsInPreview] = useToggle(false); const [argValues, setArgValues] = useState>(initialArgValues); @@ -112,15 +122,16 @@ function InitializedTemplateFunctionDialog({ type: 'tag', val: { type: 'fn', - name: templateFunction.name, + name, args: argTokens, }, }, ], }; - }, [argValues, templateFunction.name]); + }, [argValues, name]); const tagText = useTemplateTokensToString(tokens); + const templateFunction = useTemplateFunctionConfig(name, argValues, model).data; const handleDone = () => { if (tagText.data) { @@ -134,7 +145,7 @@ function InitializedTemplateFunctionDialog({ const tooLarge = rendered.data ? rendered.data.length > 10000 : false; const dataContainsSecrets = useMemo(() => { for (const [name, value] of Object.entries(argValues)) { - const arg = templateFunction.args.find((a) => 'name' in a && a.name === name); + const arg = templateFunction?.args.find((a) => 'name' in a && a.name === name); const isTextPassword = arg?.type === 'text' && arg.password; if (isTextPassword && typeof value === 'string' && value && rendered.data?.includes(value)) { return true; @@ -145,6 +156,8 @@ function InitializedTemplateFunctionDialog({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [rendered.data]); + if (templateFunction == null) return null; + return ( - {templateFunction.name === 'secure' ? ( + {name === 'secure' ? ( (function E size: 'md', title: {fn.name}(…), description: fn.description, - render: ({ hide }) => ( - { - cm.current?.view.dispatch({ - changes: [{ from: startPos, to: startPos + tagValue.length, insert }], - }); - }} - /> - ), + render: ({ hide }) => { + const model = jotaiStore.get(activeWorkspaceAtom)!; + return ( + { + cm.current?.view.dispatch({ + changes: [{ from: startPos, to: startPos + tagValue.length, insert }], + }); + }} + /> + ); + }, }); if (fn.name === 'secure') { diff --git a/src-web/hooks/useHttpAuthenticationConfig.ts b/src-web/hooks/useHttpAuthenticationConfig.ts index a18852c3..3ccad040 100644 --- a/src-web/hooks/useHttpAuthenticationConfig.ts +++ b/src-web/hooks/useHttpAuthenticationConfig.ts @@ -18,7 +18,7 @@ import { activeWorkspaceIdAtom } from './useActiveWorkspace'; export function useHttpAuthenticationConfig( authName: string | null, values: Record, - request: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace, + model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace, ) { const workspaceId = useAtomValue(activeWorkspaceIdAtom); const environmentId = useAtomValue(activeEnvironmentIdAtom); @@ -37,7 +37,7 @@ export function useHttpAuthenticationConfig( return useQuery({ queryKey: [ 'http_authentication_config', - request, + model, authName, values, responseKey, @@ -53,7 +53,7 @@ export function useHttpAuthenticationConfig( { authName, values, - request, + model, environmentId, }, ); diff --git a/src-web/hooks/useTemplateFunctionConfig.ts b/src-web/hooks/useTemplateFunctionConfig.ts new file mode 100644 index 00000000..a858ae3b --- /dev/null +++ b/src-web/hooks/useTemplateFunctionConfig.ts @@ -0,0 +1,60 @@ +import { useQuery } from '@tanstack/react-query'; +import type { + Folder, + GrpcRequest, + HttpRequest, + WebsocketRequest, + Workspace, +} from '@yaakapp-internal/models'; +import { httpResponsesAtom } from '@yaakapp-internal/models'; +import type { GetTemplateFunctionConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins'; +import { useAtomValue } from 'jotai'; +import { md5 } from 'js-md5'; +import { invokeCmd } from '../lib/tauri'; +import { activeEnvironmentIdAtom } from './useActiveEnvironment'; +import { activeWorkspaceIdAtom } from './useActiveWorkspace'; + +export function useTemplateFunctionConfig( + functionName: string | null, + values: Record, + model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace, +) { + const workspaceId = useAtomValue(activeWorkspaceIdAtom); + const environmentId = useAtomValue(activeEnvironmentIdAtom); + const responses = useAtomValue(httpResponsesAtom); + + // Some auth handlers like OAuth 2.0 show the current token after a successful request. To + // handle that, we'll force the auth to re-fetch after each new response closes + const responseKey = md5( + responses + .filter((r) => r.state === 'closed') + .map((r) => r.id) + .join(':'), + ); + + return useQuery({ + queryKey: [ + 'template_function_config', + model, + functionName, + values, + responseKey, + workspaceId, + environmentId, + ], + placeholderData: (prev) => prev, // Keep previous data on refetch + queryFn: async () => { + if (functionName == null) return null; + const config = await invokeCmd( + 'cmd_template_function_config', + { + functionName: functionName, + values, + model, + environmentId, + }, + ); + return config.function; + }, + }); +} diff --git a/src-web/hooks/useTemplateFunctions.ts b/src-web/hooks/useTemplateFunctions.ts index 6865e095..a8ae4589 100644 --- a/src-web/hooks/useTemplateFunctions.ts +++ b/src-web/hooks/useTemplateFunctions.ts @@ -1,6 +1,9 @@ import { useQuery } from '@tanstack/react-query'; -import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugins'; -import { atom, useAtomValue , useSetAtom } from 'jotai'; +import type { + GetTemplateFunctionSummaryResponse, + TemplateFunction, +} from '@yaakapp-internal/plugins'; +import { atom, useAtomValue, useSetAtom } from 'jotai'; import { useMemo, useState } from 'react'; import type { TwigCompletionOption } from '../components/core/Editor/twig/completion'; import { invokeCmd } from '../lib/tauri'; @@ -55,7 +58,9 @@ export function useSubscribeTemplateFunctions() { refetchInterval: numFns > 0 ? Infinity : 1000, refetchOnMount: true, queryFn: async () => { - const result = await invokeCmd('cmd_template_functions'); + const result = await invokeCmd( + 'cmd_template_function_summaries', + ); setNumFns(result.length); const functions = result.flatMap((r) => r.functions) ?? []; setAtom(functions); diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index eb458b6d..124ea691 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -40,7 +40,8 @@ type TauriCmd = | 'cmd_send_folder' | 'cmd_send_http_request' | 'cmd_show_workspace_key' - | 'cmd_template_functions' + | 'cmd_template_function_summaries' + | 'cmd_template_function_config' | 'cmd_template_tokens_to_string'; export async function invokeCmd(cmd: TauriCmd, args?: InvokeArgs): Promise {