gRPC request actions and "copy as gRPCurl" (#232)

This commit is contained in:
Gregory Schier
2025-07-05 15:40:41 -07:00
committed by GitHub
parent ad4d6d9720
commit 19ffcd18a6
59 changed files with 1490 additions and 320 deletions

View File

@@ -80,6 +80,7 @@ module.exports = defineConfig([
globalIgnores([ globalIgnores([
'**/node_modules/', '**/node_modules/',
'**/dist/', '**/dist/',
'**/build/',
'**/.eslintrc.cjs', '**/.eslintrc.cjs',
'**/.prettierrc.cjs', '**/.prettierrc.cjs',
'src-web/postcss.config.cjs', 'src-web/postcss.config.cjs',

592
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,8 @@
"plugins/auth-bearer", "plugins/auth-bearer",
"plugins/auth-jwt", "plugins/auth-jwt",
"plugins/auth-oauth2", "plugins/auth-oauth2",
"plugins/exporter-curl", "plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/filter-jsonpath", "plugins/filter-jsonpath",
"plugins/filter-xpath", "plugins/filter-xpath",
"plugins/importer-curl", "plugins/importer-curl",
@@ -85,6 +86,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vitest": "^3.2.4",
"workspaces-run": "^1.0.2" "workspaces-run": "^1.0.2"
} }
} }

View File

@@ -6,6 +6,10 @@ export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, }; export type BootResponse = { name: string, version: string, };
export type CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array<string>, };
export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, }; export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, }; export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
@@ -336,14 +340,14 @@ export type GetCookieValueRequest = { name: string, };
export type GetCookieValueResponse = { value: string | null, }; export type GetCookieValueResponse = { value: string | null, };
export type GetGrpcRequestActionsResponse = { actions: Array<GrpcRequestAction>, pluginRefId: string, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, }; export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, }; export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, }; export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, }; export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, }; export type GetHttpRequestByIdRequest = { id: string, };
@@ -360,6 +364,8 @@ export type GetThemesRequest = Record<string, never>;
export type GetThemesResponse = { themes: Array<Theme>, }; export type GetThemesResponse = { themes: Array<Theme>, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, }; export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, }; export type HttpHeader = { name: string, value: string, };
@@ -376,7 +382,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; 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" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "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_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": "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" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "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 JsonPrimitive = string | number | boolean | null; export type JsonPrimitive = string | number | boolean | null;
@@ -408,6 +414,10 @@ required?: boolean, };
export type PromptTextResponse = { value: string | null, }; export type PromptTextResponse = { value: string | null, };
export type RenderGrpcRequestRequest = { grpcRequest: GrpcRequest, purpose: RenderPurpose, };
export type RenderGrpcRequestResponse = { grpcRequest: GrpcRequest, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, }; export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, }; export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };

View File

@@ -9,6 +9,8 @@ import type {
OpenWindowRequest, OpenWindowRequest,
PromptTextRequest, PromptTextRequest,
PromptTextResponse, PromptTextResponse,
RenderGrpcRequestRequest,
RenderGrpcRequestResponse,
RenderHttpRequestRequest, RenderHttpRequestRequest,
RenderHttpRequestResponse, RenderHttpRequestResponse,
SendHttpRequestRequest, SendHttpRequestRequest,
@@ -45,6 +47,9 @@ export interface Context {
listNames(): Promise<ListCookieNamesResponse['names']>; listNames(): Promise<ListCookieNamesResponse['names']>;
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>; getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
}; };
grpcRequest: {
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse['grpcRequest']>;
};
httpRequest: { httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>; send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>; getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;

View File

@@ -0,0 +1,6 @@
import { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
import type { Context } from './Context';
export type GrpcRequestActionPlugin = GrpcRequestAction & {
onSelect(ctx: Context, args: CallGrpcRequestActionArgs): Promise<void> | void;
};

View File

@@ -1,5 +1,5 @@
import { ImportResources } from '../bindings/gen_events'; import { ImportResources } from '../bindings/gen_events';
import { AtLeast } from '../helpers'; import { AtLeast, MaybePromise } from '../helpers';
import type { Context } from './Context'; import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model'; type RootFields = 'name' | 'id' | 'model';
@@ -21,5 +21,8 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = { export type ImporterPlugin = {
name: string; name: string;
description?: string; description?: string;
onImport(ctx: Context, args: { text: string }): Promise<ImportPluginResponse>; onImport(
ctx: Context,
args: { text: string },
): MaybePromise<ImportPluginResponse | null | undefined>;
}; };

View File

@@ -1,5 +1,6 @@
import { AuthenticationPlugin } from './AuthenticationPlugin'; import { AuthenticationPlugin } from './AuthenticationPlugin';
import type { FilterPlugin } from './FilterPlugin'; import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin'; import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { ImporterPlugin } from './ImporterPlugin'; import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin'; import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
@@ -16,5 +17,6 @@ export type PluginDefinition = {
filter?: FilterPlugin; filter?: FilterPlugin;
authentication?: AuthenticationPlugin; authentication?: AuthenticationPlugin;
httpRequestActions?: HttpRequestActionPlugin[]; httpRequestActions?: HttpRequestActionPlugin[];
grpcRequestActions?: GrpcRequestActionPlugin[];
templateFunctions?: TemplateFunctionPlugin[]; templateFunctions?: TemplateFunctionPlugin[];
}; };

View File

@@ -7,7 +7,7 @@ import {
GetCookieValueRequest, GetCookieValueRequest,
GetCookieValueResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetHttpRequestByIdResponse,
GetKeyValueResponse, GetKeyValueResponse, GrpcRequestAction,
HttpAuthenticationAction, HttpAuthenticationAction,
HttpRequestAction, HttpRequestAction,
InternalEvent, InternalEvent,
@@ -16,6 +16,7 @@ import {
PluginWindowContext, PluginWindowContext,
PromptTextResponse, PromptTextResponse,
RenderHttpRequestResponse, RenderHttpRequestResponse,
RenderGrpcRequestResponse,
SendHttpRequestResponse, SendHttpRequestResponse,
TemplateFunction, TemplateFunction,
TemplateFunctionArg, TemplateFunctionArg,
@@ -145,6 +146,24 @@ export class PluginInstance {
return; return;
} }
if (
payload.type === 'get_grpc_request_actions_request' &&
Array.isArray(this.#mod?.grpcRequestActions)
) {
const reply: GrpcRequestAction[] = this.#mod.grpcRequestActions.map((a) => ({
...a,
// Add everything except onSelect
onSelect: undefined,
}));
const replyPayload: InternalEventPayload = {
type: 'get_grpc_request_actions_response',
pluginRefId: this.#workerData.pluginRefId,
actions: reply,
};
this.#sendPayload(windowContext, replyPayload, replyId);
return;
}
if ( if (
payload.type === 'get_http_request_actions_request' && payload.type === 'get_http_request_actions_request' &&
Array.isArray(this.#mod?.httpRequestActions) Array.isArray(this.#mod?.httpRequestActions)
@@ -208,13 +227,12 @@ export class PluginInstance {
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) { if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication; const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = []; const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) { for (const v of args) {
let v = args[i]; if (v && 'dynamic' in v) {
if ('dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload); const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v; const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput); resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else { } else if (v) {
resolvedArgs.push(v); resolvedArgs.push(v);
} }
} }
@@ -275,6 +293,18 @@ export class PluginInstance {
} }
} }
if (
payload.type === 'call_grpc_request_action_request' &&
Array.isArray(this.#mod.grpcRequestActions)
) {
const action = this.#mod.grpcRequestActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
this.#sendEmpty(windowContext, replyId);
return;
}
}
if ( if (
payload.type === 'call_template_function_request' && payload.type === 'call_template_function_request' &&
Array.isArray(this.#mod?.templateFunctions) Array.isArray(this.#mod?.templateFunctions)
@@ -472,6 +502,19 @@ export class PluginInstance {
return httpResponses; return httpResponses;
}, },
}, },
grpcRequest: {
render: async (args) => {
const payload = {
type: 'render_grpc_request_request',
...args,
} as const;
const { grpcRequest } = await this.#sendAndWaitForReply<RenderGrpcRequestResponse>(
event.windowContext,
payload,
);
return grpcRequest;
},
},
httpRequest: { httpRequest: {
getById: async (args) => { getById: async (args) => {
const payload = { const payload = {
@@ -596,20 +639,20 @@ function applyFormInputDefaults(
} }
} }
const watchedFiles: Record<string, Stats> = {}; const watchedFiles: Record<string, Stats | null> = {};
/** /**
* Watch a file and trigger callback on change. * Watch a file and trigger a callback on change.
* *
* We also track the stat for each file because fs.watch() will * We also track the stat for each file because fs.watch() will
* trigger a "change" event when the access date changes * trigger a "change" event when the access date changes.
*/ */
function watchFile(filepath: string, cb: (filepath: string) => void) { function watchFile(filepath: string, cb: () => void) {
watch(filepath, () => { watch(filepath, () => {
const stat = statSync(filepath); const stat = statSync(filepath, { throwIfNoEntry: false });
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) { if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath); watchedFiles[filepath] = stat ?? null;
cb();
} }
watchedFiles[filepath] = stat;
}); });
} }

View File

@@ -1,9 +1,10 @@
{ {
"name": "@yaak/exporter-curl", "name": "@yaak/action-copy-curl",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,18 +1,27 @@
import { HttpRequest, PluginDefinition } from '@yaakapp/api'; import type { HttpRequest, PluginDefinition } from '@yaakapp/api';
const NEWLINE = '\\\n '; const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
httpRequestActions: [{ httpRequestActions: [
label: 'Copy as Curl', {
icon: 'copy', label: 'Copy as Curl',
async onSelect(ctx, args) { icon: 'copy',
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: 'preview' }); async onSelect(ctx, args) {
const data = await convertToCurl(rendered_request); const rendered_request = await ctx.httpRequest.render({
await ctx.clipboard.copyText(data); httpRequest: args.httpRequest,
await ctx.toast.show({ message: 'Curl copied to clipboard', icon: 'copy', color: 'success' }); purpose: 'preview',
});
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: 'Command copied to clipboard',
icon: 'copy',
color: 'success',
});
},
}, },
}], ],
}; };
export async function convertToCurl(request: Partial<HttpRequest>) { export async function convertToCurl(request: Partial<HttpRequest>) {
@@ -22,7 +31,6 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
if (request.method) xs.push('-X', request.method); if (request.method) xs.push('-X', request.method);
if (request.url) xs.push(quote(request.url)); if (request.url) xs.push(quote(request.url));
xs.push(NEWLINE); xs.push(NEWLINE);
// Add URL params // Add URL params
@@ -51,7 +59,10 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
xs.push(NEWLINE); xs.push(NEWLINE);
} }
} else if (typeof request.body?.query === 'string') { } else if (typeof request.body?.query === 'string') {
const body = { query: request.body.query || '', variables: maybeParseJSON(request.body.variables, undefined) }; const body = {
query: request.body.query || '',
variables: maybeParseJSON(request.body.variables, undefined),
};
xs.push('--data-raw', `${quote(JSON.stringify(body))}`); xs.push('--data-raw', `${quote(JSON.stringify(body))}`);
xs.push(NEWLINE); xs.push(NEWLINE);
} else if (typeof request.body?.text === 'string') { } else if (typeof request.body?.text === 'string') {
@@ -84,7 +95,7 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
} }
function quote(arg: string): string { function quote(arg: string): string {
const escaped = arg.replace(/'/g, '\\\''); const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`; return `'${escaped}'`;
} }
@@ -92,10 +103,10 @@ function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name; return v.enabled !== false && !!v.name;
} }
function maybeParseJSON(v: any, fallback: any): string { function maybeParseJSON<T>(v: string, fallback: T) {
try { try {
return JSON.parse(v); return JSON.parse(v);
} catch (err) { } catch {
return fallback; return fallback;
} }
} }

View File

@@ -0,0 +1,10 @@
{
"name": "@yaak/action-copy-grpcurl",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}
}

View File

@@ -0,0 +1,134 @@
import type { GrpcRequest, PluginDefinition } from '@yaakapp/api';
import path from 'node:path';
const NEWLINE = '\\\n ';
export const plugin: PluginDefinition = {
grpcRequestActions: [
{
label: 'Copy as gRPCurl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.grpcRequest.render({
grpcRequest: args.grpcRequest,
purpose: 'preview',
});
const data = await convert(rendered_request, args.protoFiles);
await ctx.clipboard.copyText(data);
await ctx.toast.show({
message: 'Command copied to clipboard',
icon: 'copy',
color: 'success',
});
},
},
],
};
export async function convert(request: Partial<GrpcRequest>, allProtoFiles: string[]) {
const xs = ['grpcurl'];
if (request.url?.startsWith('http://')) {
xs.push('-plaintext');
}
const protoIncludes = allProtoFiles.filter((f) => !f.endsWith('.proto'));
const protoFiles = allProtoFiles.filter((f) => f.endsWith('.proto'));
const inferredIncludes = new Set<string>();
for (const f of protoFiles) {
const protoDir = findParentProtoDir(f);
if (protoDir) {
inferredIncludes.add(protoDir);
} else {
inferredIncludes.add(path.join(f, '..'));
inferredIncludes.add(path.join(f, '..', '..'));
}
}
for (const f of protoIncludes) {
xs.push('-import-path', quote(f));
xs.push(NEWLINE);
}
for (const f of inferredIncludes.values()) {
xs.push('-import-path', quote(f));
xs.push(NEWLINE);
}
for (const f of protoFiles) {
xs.push('-proto', quote(f));
xs.push(NEWLINE);
}
// Add headers
for (const h of (request.metadata ?? []).filter(onlyEnabled)) {
xs.push('-H', quote(`${h.name}: ${h.value}`));
xs.push(NEWLINE);
}
// Add basic authentication
if (request.authenticationType === 'basic') {
const user = request.authentication?.username ?? '';
const pass = request.authentication?.password ?? '';
const encoded = btoa(`${user}:${pass}`);
xs.push('-H', quote(`Authorization: Basic ${encoded}`));
xs.push(NEWLINE);
} else if (request.authenticationType === 'bearer') {
// Add bearer authentication
xs.push('-H', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));
xs.push(NEWLINE);
}
// Add form params
if (request.message) {
xs.push('-d', `${quote(JSON.stringify(JSON.parse(request.message)))}`);
xs.push(NEWLINE);
}
// Add the server address
if (request.url) {
const server = request.url.replace(/^https?:\/\//, ''); // remove protocol
xs.push(server);
}
// Add service + method
if (request.service && request.method) {
xs.push(`${request.service}/${request.method}`);
}
xs.push(NEWLINE);
// Remove trailing newline
if (xs[xs.length - 1] === NEWLINE) {
xs.splice(xs.length - 1, 1);
}
return xs.join(' ');
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function findParentProtoDir(startPath: string): string | null {
let dir = path.resolve(startPath);
while (true) {
if (path.basename(dir) === 'proto') {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) {
return null; // Reached root
}
dir = parent;
}
}

View File

@@ -0,0 +1,110 @@
import { describe, expect, test } from 'vitest';
import { convert } from '../src';
describe('exporter-curl', () => {
test('Simple example', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
},
[],
),
).toEqual([`grpcurl yaak.app`].join(` \\\n `));
});
test('Basic metadata', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
metadata: [
{ name: 'aaa', value: 'AAA' },
{ enabled: true, name: 'bbb', value: 'BBB' },
{ enabled: false, name: 'disabled', value: 'ddd' },
],
},
[],
),
).toEqual([`grpcurl -H 'aaa: AAA'`, `-H 'bbb: BBB'`, `yaak.app`].join(` \\\n `));
});
test('Single proto file', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/foo/bar/baz.proto'])).toEqual(
[
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/baz.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Multiple proto files, same dir', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/foo/bar/aaa.proto', '/foo/bar/bbb.proto']),
).toEqual(
[
`grpcurl -import-path '/foo/bar'`,
`-import-path '/foo'`,
`-proto '/foo/bar/aaa.proto'`,
`-proto '/foo/bar/bbb.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Multiple proto files, different dir', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb/ccc.proto', '/xxx/yyy/zzz.proto']),
).toEqual(
[
`grpcurl -import-path '/aaa/bbb'`,
`-import-path '/aaa'`,
`-import-path '/xxx/yyy'`,
`-import-path '/xxx'`,
`-proto '/aaa/bbb/ccc.proto'`,
`-proto '/xxx/yyy/zzz.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Single include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `yaak.app`].join(` \\\n `),
);
});
test('Multiple include dir', async () => {
expect(await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy'])).toEqual(
[`grpcurl -import-path '/aaa/bbb'`, `-import-path '/xxx/yyy'`, `yaak.app`].join(` \\\n `),
);
});
test('Mixed proto and dirs', async () => {
expect(
await convert({ url: 'https://yaak.app' }, ['/aaa/bbb', '/xxx/yyy', '/foo/bar.proto']),
).toEqual(
[
`grpcurl -import-path '/aaa/bbb'`,
`-import-path '/xxx/yyy'`,
`-import-path '/foo'`,
`-import-path '/'`,
`-proto '/foo/bar.proto'`,
`yaak.app`,
].join(` \\\n `),
);
});
test('Sends data', async () => {
expect(
await convert(
{
url: 'https://yaak.app',
message: JSON.stringify({ foo: 'bar', baz: 1.0 }, null, 2),
},
['/foo.proto'],
),
).toEqual(
[
`grpcurl -import-path '/'`,
`-proto '/foo.proto'`,
`-d '{"foo":"bar","baz":1}'`,
`yaak.app`,
].join(` \\\n `),
);
});
});

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"jsonwebtoken": "^9.0.2" "jsonwebtoken": "^9.0.2"

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"jsonpath-plus": "^10.3.0" "jsonpath-plus": "^10.3.0"

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.8.10", "@xmldom/xmldom": "^0.8.10",

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"shell-quote": "^1.8.1" "shell-quote": "^1.8.1"

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"yaml": "^2.4.2" "yaml": "^2.4.2"

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"openapi-to-postmanv2": "^5.0.0", "openapi-to-postmanv2": "^5.0.0",

View File

@@ -1,32 +1,23 @@
import { Context, Environment, Folder, HttpRequest, PluginDefinition, Workspace } from '@yaakapp/api'; import { convertPostman } from '@yaak/importer-postman/src';
import type { Context, PluginDefinition } from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
import { convert } from 'openapi-to-postmanv2'; import { convert } from 'openapi-to-postmanv2';
import { convertPostman } from '@yaakapp/importer-postman/src';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
interface ExportResources {
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
importer: { importer: {
name: 'OpenAPI', name: 'OpenAPI',
description: 'Import OpenAPI collections', description: 'Import OpenAPI collections',
onImport(_ctx: Context, args: { text: string }) { onImport(_ctx: Context, args: { text: string }) {
return convertOpenApi(args.text) as any; return convertOpenApi(args.text);
}, },
}, },
}; };
export async function convertOpenApi( export async function convertOpenApi(contents: string): Promise<ImportPluginResponse | undefined> {
contents: string,
): Promise<{ resources: ExportResources } | undefined> {
let postmanCollection; let postmanCollection;
try { try {
postmanCollection = await new Promise((resolve, reject) => { postmanCollection = await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
convert({ type: 'string', data: contents }, {}, (err, result: any) => { convert({ type: 'string', data: contents }, {}, (err, result: any) => {
if (err != null) reject(err); if (err != null) reject(err);
@@ -35,7 +26,7 @@ export async function convertOpenApi(
} }
}); });
}); });
} catch (err) { } catch {
// Probably not an OpenAPI file, so skip it // Probably not an OpenAPI file, so skip it
return undefined; return undefined;
} }

View File

@@ -5,6 +5,7 @@
"main": "./build/index.js", "main": "./build/index.js",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,13 +1,15 @@
import { import type {
Context, Context,
Environment, Environment,
Folder, Folder,
HttpRequest, HttpRequest,
HttpRequestHeader, HttpRequestHeader,
HttpUrlParameter, HttpUrlParameter,
PartialImportResources,
PluginDefinition, PluginDefinition,
Workspace, Workspace,
} from '@yaakapp/api'; } from '@yaakapp/api';
import type { ImportPluginResponse } from '@yaakapp/api/lib/plugins/ImporterPlugin';
const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'; const POSTMAN_2_1_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json'; const POSTMAN_2_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
@@ -27,19 +29,19 @@ export const plugin: PluginDefinition = {
name: 'Postman', name: 'Postman',
description: 'Import postman collections', description: 'Import postman collections',
onImport(_ctx: Context, args: { text: string }) { onImport(_ctx: Context, args: { text: string }) {
return convertPostman(args.text) as any; return convertPostman(args.text);
}, },
}, },
}; };
export function convertPostman( export function convertPostman(contents: string): ImportPluginResponse | undefined {
contents: string,
): { resources: ExportResources } | undefined {
const root = parseJSONToRecord(contents); const root = parseJSONToRecord(contents);
if (root == null) return; if (root == null) return;
const info = toRecord(root.info); const info = toRecord(root.info);
const isValidSchema = VALID_SCHEMAS.includes(info.schema); const isValidSchema = VALID_SCHEMAS.includes(
typeof info.schema === 'string' ? info.schema : 'n/a',
);
if (!isValidSchema || !Array.isArray(root.item)) { if (!isValidSchema || !Array.isArray(root.item)) {
return; return;
} }
@@ -53,11 +55,17 @@ export function convertPostman(
folders: [], folders: [],
}; };
const rawDescription = info.description;
const description =
typeof rawDescription === 'object' && rawDescription !== null && 'content' in rawDescription
? String(rawDescription.content)
: String(rawDescription);
const workspace: ExportResources['workspaces'][0] = { const workspace: ExportResources['workspaces'][0] = {
model: 'workspace', model: 'workspace',
id: generateId('workspace'), id: generateId('workspace'),
name: info.name || 'Postman Import', name: info.name ? String(info.name) : 'Postman Import',
description: info.description?.content ?? info.description, description,
}; };
exportResources.workspaces.push(workspace); exportResources.workspaces.push(workspace);
@@ -68,14 +76,14 @@ export function convertPostman(
name: 'Global Variables', name: 'Global Variables',
workspaceId: workspace.id, workspaceId: workspace.id,
variables: variables:
root.variable?.map((v: any) => ({ toArray<{ key: string; value: string }>(root.variable).map((v) => ({
name: v.key, name: v.key,
value: v.value, value: v.value,
})) ?? [], })) ?? [],
}; };
exportResources.environments.push(environment); exportResources.environments.push(environment);
const importItem = (v: Record<string, any>, folderId: string | null = null) => { const importItem = (v: Record<string, unknown>, folderId: string | null = null) => {
if (typeof v.name === 'string' && Array.isArray(v.item)) { if (typeof v.name === 'string' && Array.isArray(v.item)) {
const folder: ExportResources['folders'][0] = { const folder: ExportResources['folders'][0] = {
model: 'folder', model: 'folder',
@@ -94,7 +102,11 @@ export function convertPostman(
const requestAuthPath = importAuth(r.auth); const requestAuthPath = importAuth(r.auth);
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath; const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
const headers: HttpRequestHeader[] = toArray(r.header).map((h) => { const headers: HttpRequestHeader[] = toArray<{
key: string;
value: string;
disabled?: boolean;
}>(r.header).map((h) => {
return { return {
name: h.key, name: h.key,
value: h.value, value: h.value,
@@ -104,7 +116,9 @@ export function convertPostman(
// Add body headers only if they don't already exist // Add body headers only if they don't already exist
for (const bodyPatchHeader of bodyPatch.headers) { for (const bodyPatchHeader of bodyPatch.headers) {
const existingHeader = headers.find(h => h.name.toLowerCase() === bodyPatchHeader.name.toLowerCase()); const existingHeader = headers.find(
(h) => h.name.toLowerCase() === bodyPatchHeader.name.toLowerCase(),
);
if (existingHeader) { if (existingHeader) {
continue; continue;
} }
@@ -119,8 +133,8 @@ export function convertPostman(
workspaceId: workspace.id, workspaceId: workspace.id,
folderId, folderId,
name: v.name, name: v.name,
description: v.description || undefined, description: v.description ? String(v.description) : undefined,
method: r.method || 'GET', method: typeof r.method === 'string' ? r.method : 'GET',
url, url,
urlParameters, urlParameters,
body: bodyPatch.body, body: bodyPatch.body,
@@ -139,17 +153,19 @@ export function convertPostman(
importItem(item); importItem(item);
} }
const resources = deleteUndefinedAttrs(convertTemplateSyntax(exportResources)); const resources = deleteUndefinedAttrs(
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources }; return { resources };
} }
function convertUrl(url: string | any): Pick<HttpRequest, 'url' | 'urlParameters'> { function convertUrl(rawUrl: string | unknown): Pick<HttpRequest, 'url' | 'urlParameters'> {
if (typeof url === 'string') { if (typeof rawUrl === 'string') {
return { url, urlParameters: [] }; return { url: rawUrl, urlParameters: [] };
} }
url = toRecord(url); const url = toRecord(rawUrl);
let v = ''; let v = '';
@@ -199,10 +215,8 @@ function convertUrl(url: string | any): Pick<HttpRequest, 'url' | 'urlParameters
return { url: v, urlParameters: params }; return { url: v, urlParameters: params };
} }
function importAuth( function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
rawAuth: any, const auth = toRecord<{ username?: string; password?: string; token?: string }>(rawAuth);
): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
const auth = toRecord(rawAuth);
if ('basic' in auth) { if ('basic' in auth) {
return { return {
authenticationType: 'basic', authenticationType: 'basic',
@@ -223,8 +237,22 @@ function importAuth(
} }
} }
function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> { function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
const body = toRecord(rawBody); const body = toRecord(rawBody) as {
mode: string;
graphql: { query?: string; variables?: string };
urlencoded?: { key?: string; value?: string; disabled?: boolean }[];
formdata?: {
key?: string;
value?: string;
disabled?: boolean;
contentType?: string;
src?: string;
}[];
raw?: string;
options?: { raw?: { language?: string } };
file?: { src?: string };
};
if (body.mode === 'graphql') { if (body.mode === 'graphql') {
return { return {
headers: [ headers: [
@@ -237,7 +265,10 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
bodyType: 'graphql', bodyType: 'graphql',
body: { body: {
text: JSON.stringify( text: JSON.stringify(
{ query: body.graphql.query, variables: parseJSONToRecord(body.graphql.variables) }, {
query: body.graphql?.query || '',
variables: parseJSONToRecord(body.graphql?.variables || '{}'),
},
null, null,
2, 2,
), ),
@@ -254,7 +285,7 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
], ],
bodyType: 'application/x-www-form-urlencoded', bodyType: 'application/x-www-form-urlencoded',
body: { body: {
form: toArray(body.urlencoded).map((f) => ({ form: toArray<NonNullable<typeof body.urlencoded>[0]>(body.urlencoded).map((f) => ({
enabled: !f.disabled, enabled: !f.disabled,
name: f.key ?? '', name: f.key ?? '',
value: f.value ?? '', value: f.value ?? '',
@@ -272,19 +303,19 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
], ],
bodyType: 'multipart/form-data', bodyType: 'multipart/form-data',
body: { body: {
form: toArray(body.formdata).map((f) => form: toArray<NonNullable<typeof body.formdata>[0]>(body.formdata).map((f) =>
f.src != null f.src != null
? { ? {
enabled: !f.disabled, enabled: !f.disabled,
contentType: f.contentType ?? null, contentType: f.contentType ?? null,
name: f.key ?? '', name: f.key ?? '',
file: f.src ?? '', file: f.src ?? '',
} }
: { : {
enabled: !f.disabled, enabled: !f.disabled,
name: f.key ?? '', name: f.key ?? '',
value: f.value ?? '', value: f.value ?? '',
}, },
), ),
}, },
}; };
@@ -315,21 +346,23 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
} }
} }
function parseJSONToRecord(jsonStr: string): Record<string, any> | null { function parseJSONToRecord<T>(jsonStr: string): Record<string, T> | null {
try { try {
return toRecord(JSON.parse(jsonStr)); return toRecord(JSON.parse(jsonStr));
} catch (err) { } catch {
return null;
} }
return null;
} }
function toRecord(value: any): Record<string, any> { function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
if (Object.prototype.toString.call(value) === '[object Object]') return value; if (value && typeof value === 'object' && !Array.isArray(value)) {
else return {}; return value as Record<string, T>;
}
return {};
} }
function toArray(value: any): any[] { function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value; if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return []; else return [];
} }

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.js", "build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,4 +1,4 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [ templateFunctions: [
@@ -7,7 +7,7 @@ export const plugin: PluginDefinition = {
description: 'Encode a value to base64', description: 'Encode a value to base64',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }], args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return Buffer.from(args.values.value ?? '').toString('base64'); return Buffer.from(String(args.values.value ?? '')).toString('base64');
}, },
}, },
{ {
@@ -15,7 +15,7 @@ export const plugin: PluginDefinition = {
description: 'Decode a value from base64', description: 'Decode a value from base64',
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }], args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return Buffer.from(args.values.value ?? '', 'base64').toString('utf-8'); return Buffer.from(String(args.values.value ?? ''), 'base64').toString('utf-8');
}, },
}, },
{ {
@@ -23,7 +23,7 @@ export const plugin: PluginDefinition = {
description: 'Encode a value for use in a URL (percent-encoding)', description: 'Encode a value for use in a URL (percent-encoding)',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }], args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
return encodeURIComponent(args.values.value ?? ''); return encodeURIComponent(String(args.values.value ?? ''));
}, },
}, },
{ {
@@ -32,7 +32,7 @@ export const plugin: PluginDefinition = {
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }], args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try { try {
return decodeURIComponent(args.values.value ?? ''); return decodeURIComponent(String(args.values.value ?? ''));
} catch { } catch {
return ''; return '';
} }

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,19 +1,21 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
import fs from 'node:fs'; import fs from 'node:fs';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [{ templateFunctions: [
name: 'fs.readFile', {
description: 'Read the contents of a file as utf-8', name: 'fs.readFile',
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }], description: 'Read the contents of a file as utf-8',
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }],
if (!args.values.path) return null; async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path) return null;
try { try {
return fs.promises.readFile(args.values.path, 'utf-8'); return fs.promises.readFile(String(args.values.path ?? ''), 'utf-8');
} catch (err) { } catch {
return null; return null;
} }
},
}, },
}], ],
}; };

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"jsonpath-plus": "^10.3.0" "jsonpath-plus": "^10.3.0"

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,4 +1,4 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [{ templateFunctions: [{
@@ -15,10 +15,10 @@ export const plugin: PluginDefinition = {
return await ctx.prompt.text({ return await ctx.prompt.text({
id: `prompt-${args.values.label}`, id: `prompt-${args.values.label}`,
label: args.values.title ?? '', label: String(args.values.title ?? ''),
title: args.values.title ?? '', title: String(args.values.title ?? ''),
defaultValue: args.values.defaultValue, defaultValue: String(args.values.defaultValue),
placeholder: args.values.placeholder, placeholder: String(args.values.placeholder),
}); });
}, },
}], }],

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,28 +1,32 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [{ templateFunctions: [
name: 'regex.match', {
description: 'Extract', name: 'regex.match',
args: [ description: 'Extract',
{ args: [
type: 'text', {
name: 'regex', type: 'text',
label: 'Regular Expression', name: 'regex',
placeholder: '^\w+=(?<value>\w*)$', label: 'Regular Expression',
defaultValue: '^(.*)$', placeholder: '^\\w+=(?<value>\\w*)$',
description: 'A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values.', defaultValue: '^(.*)$',
}, description:
{ type: 'text', name: 'input', label: 'Input Text', multiLine: true }, 'A JavaScript regular expression, evaluated using the Node.js RegExp engine. Capture groups or named groups can be used to extract values.',
], },
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { { type: 'text', name: 'input', label: 'Input Text', multiLine: true },
if (!args.values.regex) return ''; ],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.regex || !args.values.input) return '';
const regex = new RegExp(String(args.values.regex)); const input = String(args.values.input);
const match = args.values.input?.match(regex); const regex = new RegExp(String(args.values.regex));
return match?.groups const match = input.match(regex);
? Object.values(match.groups)[0] ?? '' return match?.groups
: match?.[1] ?? match?.[0] ?? ''; ? (Object.values(match.groups)[0] ?? '')
: (match?.[1] ?? match?.[0] ?? '');
},
}, },
}], ],
}; };

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -1,21 +1,26 @@
import { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api'; import type { CallTemplateFunctionArgs, Context, PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
templateFunctions: [ templateFunctions: [
{ {
name: 'request.body', name: 'request.body',
args: [{ args: [
name: 'requestId', {
label: 'Http Request', name: 'requestId',
type: 'http_request', label: 'Http Request',
}], type: 'http_request',
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? 'n/a' }); const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null; if (httpRequest == null) return null;
return String(await ctx.templates.render({ return String(
data: httpRequest.body?.text ?? '', await ctx.templates.render({
purpose: args.purpose, data: httpRequest.body?.text ?? '',
})); purpose: args.purpose,
}),
);
}, },
}, },
{ {
@@ -30,15 +35,22 @@ export const plugin: PluginDefinition = {
name: 'header', name: 'header',
label: 'Header Name', label: 'Header Name',
type: 'text', type: 'text',
}], },
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
const httpRequest = await ctx.httpRequest.getById({ id: args.values.requestId ?? 'n/a' }); const headerName = String(args.values.header ?? '');
const requestId = String(args.values.requestId ?? 'n/a');
const httpRequest = await ctx.httpRequest.getById({ id: requestId });
if (httpRequest == null) return null; if (httpRequest == null) return null;
const header = httpRequest.headers.find(h => h.name.toLowerCase() === args.values.header?.toLowerCase()); const header = httpRequest.headers.find(
return String(await ctx.templates.render({ (h) => h.name.toLowerCase() === headerName.toLowerCase(),
data: header?.value ?? '', );
purpose: args.purpose, return String(
})); await ctx.templates.render({
data: header?.value ?? '',
purpose: args.purpose,
}),
);
}, },
}, },
], ],

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",

View File

@@ -1,5 +1,5 @@
import { DOMParser } from '@xmldom/xmldom'; import { DOMParser } from '@xmldom/xmldom';
import { import type {
CallTemplateFunctionArgs, CallTemplateFunctionArgs,
Context, Context,
FormInput, FormInput,
@@ -47,14 +47,14 @@ export const plugin: PluginDefinition = {
if (!args.values.request || !args.values.header) return null; if (!args.values.request || !args.values.header) return null;
const response = await getResponse(ctx, { const response = await getResponse(ctx, {
requestId: args.values.request, requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ?? null, behavior: args.values.behavior ? String(args.values.behavior) : null,
}); });
if (response == null) return null; if (response == null) return null;
const header = response.headers.find( const header = response.headers.find(
h => h.name.toLowerCase() === String(args.values.header ?? '').toLowerCase(), (h) => h.name.toLowerCase() === String(args.values.header ?? '').toLowerCase(),
); );
return header?.value ?? null; return header?.value ?? null;
}, },
@@ -77,9 +77,9 @@ export const plugin: PluginDefinition = {
if (!args.values.request || !args.values.path) return null; if (!args.values.request || !args.values.path) return null;
const response = await getResponse(ctx, { const response = await getResponse(ctx, {
requestId: args.values.request, requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ?? null, behavior: args.values.behavior ? String(args.values.behavior) : null,
}); });
if (response == null) return null; if (response == null) return null;
@@ -90,19 +90,19 @@ export const plugin: PluginDefinition = {
let body; let body;
try { try {
body = readFileSync(response.bodyPath, 'utf-8'); body = readFileSync(response.bodyPath, 'utf-8');
} catch (_) { } catch {
return null; return null;
} }
try { try {
return filterJSONPath(body, args.values.path); return filterJSONPath(body, String(args.values.path || ''));
} catch (err) { } catch {
// Probably not JSON, try XPath // Probably not JSON, try XPath
} }
try { try {
return filterXPath(body, args.values.path); return filterXPath(body, String(args.values.path || ''));
} catch (err) { } catch {
// Probably not XML // Probably not XML
} }
@@ -113,17 +113,14 @@ export const plugin: PluginDefinition = {
name: 'response.body.raw', name: 'response.body.raw',
description: 'Access the entire response body, as text', description: 'Access the entire response body, as text',
aliases: ['response'], aliases: ['response'],
args: [ args: [requestArg, behaviorArg],
requestArg,
behaviorArg,
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> { async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request) return null; if (!args.values.request) return null;
const response = await getResponse(ctx, { const response = await getResponse(ctx, {
requestId: args.values.request, requestId: String(args.values.request || ''),
purpose: args.purpose, purpose: args.purpose,
behavior: args.values.behavior ?? null, behavior: args.values.behavior ? String(args.values.behavior) : null,
}); });
if (response == null) return null; if (response == null) return null;
@@ -134,7 +131,7 @@ export const plugin: PluginDefinition = {
let body; let body;
try { try {
body = readFileSync(response.bodyPath, 'utf-8'); body = readFileSync(response.bodyPath, 'utf-8');
} catch (_) { } catch {
return null; return null;
} }
@@ -173,11 +170,18 @@ function filterXPath(body: string, path: string): string {
} }
} }
async function getResponse(ctx: Context, { requestId, behavior, purpose }: { async function getResponse(
requestId: string, ctx: Context,
behavior: string | null, {
purpose: RenderPurpose, requestId,
}): Promise<HttpResponse | null> { behavior,
purpose,
}: {
requestId: string;
behavior: string | null;
purpose: RenderPurpose;
},
): Promise<HttpResponse | null> {
if (!requestId) return null; if (!requestId) return null;
const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? 'n/a' }); const httpRequest = await ctx.httpRequest.getById({ id: requestId ?? 'n/a' });
@@ -195,9 +199,7 @@ async function getResponse(ctx: Context, { requestId, behavior, purpose }: {
// Previews happen a ton, and we don't want to send too many times on "always," so treat // Previews happen a ton, and we don't want to send too many times on "always," so treat
// it as "smart" during preview. // it as "smart" during preview.
let finalBehavior = (behavior === 'always' && purpose === 'preview') const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
? 'smart'
: behavior;
// Send if no responses and "smart," or "always" // Send if no responses and "smart," or "always"
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') { if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') {

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"uuid": "^11.1.0" "uuid": "^11.1.0"

View File

@@ -4,7 +4,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@xmldom/xmldom": "^0.8.10", "@xmldom/xmldom": "^0.8.10",

View File

@@ -4,6 +4,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"build": "yaakcli build ./src/index.ts", "build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js" "dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
} }
} }

View File

@@ -36,7 +36,8 @@ use yaak_models::models::{
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources}; use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{ use yaak_plugins::events::{
CallHttpRequestActionRequest, FilterResponse, GetHttpAuthenticationConfigResponse, CallGrpcRequestActionRequest, CallHttpRequestActionRequest, FilterResponse,
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive, GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive,
PluginWindowContext, RenderPurpose, PluginWindowContext, RenderPurpose,
@@ -791,6 +792,14 @@ async fn cmd_http_request_actions<R: Runtime>(
Ok(plugin_manager.get_http_request_actions(&window).await?) Ok(plugin_manager.get_http_request_actions(&window).await?)
} }
#[tauri::command]
async fn cmd_grpc_request_actions<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<Vec<GetGrpcRequestActionsResponse>> {
Ok(plugin_manager.get_grpc_request_actions(&window).await?)
}
#[tauri::command] #[tauri::command]
async fn cmd_template_functions<R: Runtime>( async fn cmd_template_functions<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
@@ -830,6 +839,15 @@ async fn cmd_call_http_request_action<R: Runtime>(
Ok(plugin_manager.call_http_request_action(&window, req).await?) Ok(plugin_manager.call_http_request_action(&window, req).await?)
} }
#[tauri::command]
async fn cmd_call_grpc_request_action<R: Runtime>(
window: WebviewWindow<R>,
req: CallGrpcRequestActionRequest,
plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> {
Ok(plugin_manager.call_grpc_request_action(&window, req).await?)
}
#[tauri::command] #[tauri::command]
async fn cmd_call_http_authentication_action<R: Runtime>( async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
@@ -1220,6 +1238,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action, cmd_call_http_authentication_action,
cmd_call_http_request_action, cmd_call_http_request_action,
cmd_call_grpc_request_action,
cmd_check_for_updates, cmd_check_for_updates,
cmd_create_grpc_request, cmd_create_grpc_request,
cmd_curl_to_request, cmd_curl_to_request,
@@ -1236,6 +1255,7 @@ pub fn run() {
cmd_get_workspace_meta, cmd_get_workspace_meta,
cmd_grpc_go, cmd_grpc_go,
cmd_grpc_reflect, cmd_grpc_reflect,
cmd_grpc_request_actions,
cmd_http_request_actions, cmd_http_request_actions,
cmd_import_data, cmd_import_data,
cmd_install_plugin, cmd_install_plugin,

View File

@@ -1,6 +1,6 @@
use crate::http_request::send_http_request; use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value}; use crate::render::{render_grpc_request, render_http_request, render_json_value};
use crate::window::{CreateWindowConfig, create_window}; use crate::window::{create_window, CreateWindowConfig};
use crate::{ use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context, call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window, workspace_from_window,
@@ -13,13 +13,7 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin}; use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::query_manager::QueryManagerExt; use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource; use yaak_models::util::UpdateSource;
use yaak_plugins::events::{ use yaak_plugins::events::{Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload, ListCookieNamesResponse, PluginWindowContext, RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent};
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
ListCookieNamesResponse, PluginWindowContext, RenderHttpRequestResponse,
SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse,
WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -68,6 +62,30 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
http_request, http_request,
})) }))
} }
InternalEventPayload::RenderGrpcRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render grpc request");
let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window);
let base_environment = app_handle
.db()
.get_base_environment(&workspace.id)
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let grpc_request = render_grpc_request(
&req.grpc_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await
.expect("Failed to render grpc request");
Some(InternalEventPayload::RenderGrpcRequestResponse(RenderGrpcRequestResponse {
grpc_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => { InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context) let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request"); .expect("Failed to find window for render http request");

View File

@@ -47,9 +47,14 @@ pub async fn fill_pool_from_files(
]; ];
for p in paths { for p in paths {
if p.as_path().exists() { if !p.exists() {
continue;
}
// Dirs are added as includes
if p.is_dir() {
args.push("-I".to_string());
args.push(p.to_string_lossy().to_string()); args.push(p.to_string_lossy().to_string());
} else {
continue; continue;
} }
@@ -62,6 +67,8 @@ pub async fn fill_pool_from_files(
} else { } else {
debug!("ignoring {:?} since it does not exist.", parent) debug!("ignoring {:?} since it does not exist.", parent)
} }
args.push(p.to_string_lossy().to_string());
} }
let out = app_handle let out = app_handle

View File

@@ -6,6 +6,10 @@ export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, }; export type BootResponse = { name: string, version: string, };
export type CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array<string>, };
export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, }; export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, }; export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
@@ -336,14 +340,14 @@ export type GetCookieValueRequest = { name: string, };
export type GetCookieValueResponse = { value: string | null, }; export type GetCookieValueResponse = { value: string | null, };
export type GetGrpcRequestActionsResponse = { actions: Array<GrpcRequestAction>, pluginRefId: string, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, }; export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, }; export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, }; export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, }; export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, }; export type GetHttpRequestByIdRequest = { id: string, };
@@ -360,6 +364,8 @@ export type GetThemesRequest = Record<string, never>;
export type GetThemesResponse = { themes: Array<Theme>, }; export type GetThemesResponse = { themes: Array<Theme>, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, }; export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, }; export type HttpHeader = { name: string, value: string, };
@@ -376,7 +382,7 @@ export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: PluginWindowContext, payload: InternalEventPayload, }; 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" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "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_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": "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" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & BootResponse | { "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 JsonPrimitive = string | number | boolean | null; export type JsonPrimitive = string | number | boolean | null;
@@ -408,6 +414,10 @@ required?: boolean, };
export type PromptTextResponse = { value: string | null, }; export type PromptTextResponse = { value: string | null, };
export type RenderGrpcRequestRequest = { grpcRequest: GrpcRequest, purpose: RenderPurpose, };
export type RenderGrpcRequestResponse = { grpcRequest: GrpcRequest, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, }; export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, }; export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };

View File

@@ -89,11 +89,16 @@ pub enum InternalEventPayload {
GetCookieValueRequest(GetCookieValueRequest), GetCookieValueRequest(GetCookieValueRequest),
GetCookieValueResponse(GetCookieValueResponse), GetCookieValueResponse(GetCookieValueResponse),
// Request Actions // HTTP Request Actions
GetHttpRequestActionsRequest(EmptyPayload), GetHttpRequestActionsRequest(EmptyPayload),
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse), GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest), CallHttpRequestActionRequest(CallHttpRequestActionRequest),
// Grpc Request Actions
GetGrpcRequestActionsRequest(EmptyPayload),
GetGrpcRequestActionsResponse(GetGrpcRequestActionsResponse),
CallGrpcRequestActionRequest(CallGrpcRequestActionRequest),
// Template Functions // Template Functions
GetTemplateFunctionsRequest, GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse), GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
@@ -116,6 +121,9 @@ pub enum InternalEventPayload {
RenderHttpRequestRequest(RenderHttpRequestRequest), RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse), RenderHttpRequestResponse(RenderHttpRequestResponse),
RenderGrpcRequestRequest(RenderGrpcRequestRequest),
RenderGrpcRequestResponse(RenderGrpcRequestResponse),
GetKeyValueRequest(GetKeyValueRequest), GetKeyValueRequest(GetKeyValueRequest),
GetKeyValueResponse(GetKeyValueResponse), GetKeyValueResponse(GetKeyValueResponse),
SetKeyValueRequest(SetKeyValueRequest), SetKeyValueRequest(SetKeyValueRequest),
@@ -287,6 +295,21 @@ pub struct RenderHttpRequestResponse {
pub http_request: HttpRequest, pub http_request: HttpRequest,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct RenderGrpcRequestRequest {
pub grpc_request: GrpcRequest,
pub purpose: RenderPurpose,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct RenderGrpcRequestResponse {
pub grpc_request: GrpcRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]
@@ -967,11 +990,6 @@ impl Default for RenderPurpose {
} }
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestActionsRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]
@@ -1005,6 +1023,40 @@ pub struct CallHttpRequestActionArgs {
pub http_request: HttpRequest, pub http_request: HttpRequest,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetGrpcRequestActionsResponse {
pub actions: Vec<GrpcRequestAction>,
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 GrpcRequestAction {
pub label: String,
#[ts(optional)]
pub icon: Option<Icon>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallGrpcRequestActionRequest {
pub index: i32,
pub plugin_ref_id: String,
pub args: CallGrpcRequestActionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallGrpcRequestActionArgs {
pub grpc_request: GrpcRequest,
pub proto_files: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")] #[ts(export, export_to = "gen_events.ts")]

View File

@@ -3,10 +3,11 @@ use crate::error::Error::{
}; };
use crate::error::Result; use crate::error::Result;
use crate::events::{ use crate::events::{
BootRequest, CallHttpAuthenticationActionArgs, CallHttpAuthenticationActionRequest, BootRequest, CallGrpcRequestActionRequest, CallHttpAuthenticationActionArgs,
CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallHttpAuthenticationActionRequest, CallHttpAuthenticationRequest,
CallTemplateFunctionArgs, CallTemplateFunctionRequest, CallTemplateFunctionResponse, CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
EmptyPayload, FilterRequest, FilterResponse, GetHttpAuthenticationConfigRequest, CallTemplateFunctionRequest, CallTemplateFunctionResponse, EmptyPayload, FilterRequest,
FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, GetThemesRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, GetThemesRequest,
GetThemesResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, GetThemesResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
@@ -426,6 +427,27 @@ impl PluginManager {
Ok(themes) Ok(themes)
} }
pub async fn get_grpc_request_actions<R: Runtime>(
&self,
window: &WebviewWindow<R>,
) -> Result<Vec<GetGrpcRequestActionsResponse>> {
let reply_events = self
.send_and_wait(
&PluginWindowContext::new(window),
&InternalEventPayload::GetGrpcRequestActionsRequest(EmptyPayload {}),
)
.await?;
let mut all_actions = Vec::new();
for event in reply_events {
if let InternalEventPayload::GetGrpcRequestActionsResponse(resp) = event.payload {
all_actions.push(resp.clone());
}
}
Ok(all_actions)
}
pub async fn get_http_request_actions<R: Runtime>( pub async fn get_http_request_actions<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
@@ -495,6 +517,23 @@ impl PluginManager {
Ok(()) Ok(())
} }
pub async fn call_grpc_request_action<R: Runtime>(
&self,
window: &WebviewWindow<R>,
req: CallGrpcRequestActionRequest,
) -> Result<()> {
let ref_id = req.plugin_ref_id.clone();
let plugin =
self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?;
let event = plugin.build_event_to_send(
&PluginWindowContext::new(window),
&InternalEventPayload::CallGrpcRequestActionRequest(req),
None,
);
plugin.send(&event).await?;
Ok(())
}
pub async fn get_http_authentication_summaries<R: Runtime>( pub async fn get_http_authentication_summaries<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,

View File

@@ -10,6 +10,7 @@ import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { Link } from './core/Link'; import { Link } from './core/Link';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { Icon } from './core/Icon';
interface Props { interface Props {
onDone: () => void; onDone: () => void;
@@ -45,6 +46,7 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
<HStack space={2} justifyContent="start" className="flex-row-reverse"> <HStack space={2} justifyContent="start" className="flex-row-reverse">
<Button <Button
color="primary" color="primary"
variant="border"
onClick={async () => { onClick={async () => {
const selected = await open({ const selected = await open({
title: 'Select Proto Files', title: 'Select Proto Files',
@@ -58,11 +60,28 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
await grpc.reflect.refetch(); await grpc.reflect.refetch();
}} }}
> >
Add Proto File(s) Add Files
</Button>
<Button
variant="border"
color="primary"
onClick={async () => {
const selected = await open({
title: 'Select Proto Directory',
directory: true,
});
if (selected == null) return;
await protoFilesKv.set([...protoFiles.filter((f) => f !== selected), selected]);
await grpc.reflect.refetch();
}}
>
Add Directories
</Button> </Button>
<Button <Button
isLoading={grpc.reflect.isFetching} isLoading={grpc.reflect.isFetching}
disabled={grpc.reflect.isFetching} disabled={grpc.reflect.isFetching}
variant="border"
color="secondary" color="secondary"
onClick={() => grpc.reflect.refetch()} onClick={() => grpc.reflect.refetch()}
> >
@@ -70,6 +89,14 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
</Button> </Button>
</HStack> </HStack>
<VStack space={5}> <VStack space={5}>
{reflectError && (
<Banner color="warning">
<h1 className="font-bold">
Reflection failed on URL <InlineCode>{request.url || 'n/a'}</InlineCode>
</h1>
<p>{reflectError.trim()}</p>
</Banner>
)}
{!serverReflection && services != null && services.length > 0 && ( {!serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2"> <Banner className="flex flex-col gap-2">
<p> <p>
@@ -108,39 +135,41 @@ function GrpcProtoSelectionDialogWithRequest({ request }: Props & { request: Grp
<table className="w-full divide-y divide-surface-highlight"> <table className="w-full divide-y divide-surface-highlight">
<thead> <thead>
<tr> <tr>
<th className="text-text-subtlest"> <th />
Added Files <th className="text-text-subtlest">Added File Paths</th>
</th> <th />
<th/>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-surface-highlight"> <tbody className="divide-y divide-surface-highlight">
{protoFiles.map((f, i) => ( {protoFiles.map((f, i) => {
<tr key={f + i} className="group"> const parts = f.split('/');
<td className="pl-1 font-mono text-sm" title={f}>{f.split('/').pop()}</td> return (
<td className="w-0 py-0.5"> <tr key={f + i} className="group">
<IconButton <td>
title="Remove file" <Icon icon={f.endsWith('.proto') ? 'file_code' : 'folder_code'} />
icon="trash" </td>
className="ml-auto opacity-50 transition-opacity group-hover:opacity-100" <td className="pl-1 font-mono text-sm" title={f}>
onClick={async () => { {parts.length > 3 && '.../'}
await protoFilesKv.set(protoFiles.filter((p) => p !== f)); {parts.slice(-3).join('/')}
}} </td>
/> <td className="w-0 py-0.5">
</td> <IconButton
</tr> title="Remove file"
))} variant="border"
size="xs"
icon="trash"
className="my-0.5 ml-auto opacity-50 transition-opacity group-hover:opacity-100"
onClick={async () => {
await protoFilesKv.set(protoFiles.filter((p) => p !== f));
}}
/>
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
)} )}
{reflectError && (
<Banner color="warning">
<h1 className="font-bold">
Reflection failed on URL <InlineCode>{request.url || 'n/a'}</InlineCode>
</h1>
{reflectError}
</Banner>
)}
{reflectionUnimplemented && protoFiles.length === 0 && ( {reflectionUnimplemented && protoFiles.length === 0 && (
<Banner> <Banner>
<InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '} <InlineCode>{request.url}</InlineCode> doesn&apos;t implement{' '}

View File

@@ -14,7 +14,6 @@ export function Banner({ children, className, color }: BannerProps) {
className={classNames( className={classNames(
className, className,
`x-theme-banner--${color}`, `x-theme-banner--${color}`,
'whitespace-pre-wrap',
'border border-border bg-surface', 'border border-border bg-surface',
'px-4 py-3 rounded-lg select-auto', 'px-4 py-3 rounded-lg select-auto',
'overflow-auto text-text', 'overflow-auto text-text',

View File

@@ -45,6 +45,7 @@ const icons = {
eye: lucide.EyeIcon, eye: lucide.EyeIcon,
eye_closed: lucide.EyeOffIcon, eye_closed: lucide.EyeOffIcon,
file_code: lucide.FileCodeIcon, file_code: lucide.FileCodeIcon,
folder_code: lucide.FolderCodeIcon,
filter: lucide.FilterIcon, filter: lucide.FilterIcon,
flame: lucide.FlameIcon, flame: lucide.FlameIcon,
flask: lucide.FlaskConicalIcon, flask: lucide.FlaskConicalIcon,
@@ -134,6 +135,8 @@ export const Icon = memo(function Icon({
title={title} title={title}
className={classNames( className={classNames(
className, className,
!spin && 'transform-cpu',
spin && 'animate-spin',
'flex-shrink-0', 'flex-shrink-0',
size === 'xl' && 'h-6 w-6', size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5', size === 'lg' && 'h-5 w-5',
@@ -149,7 +152,6 @@ export const Icon = memo(function Icon({
color === 'success' && 'text-success', color === 'success' && 'text-success',
color === 'primary' && 'text-primary', color === 'primary' && 'text-primary',
color === 'secondary' && 'text-secondary', color === 'secondary' && 'text-secondary',
spin && 'animate-spin',
)} )}
/> />
); );

View File

@@ -3,6 +3,7 @@ import { useAtomValue } from 'jotai';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { openFolderSettings } from '../../commands/openFolderSettings'; import { openFolderSettings } from '../../commands/openFolderSettings';
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems'; import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
import { useGrpcRequestActions } from '../../hooks/useGrpcRequestActions';
import { useHttpRequestActions } from '../../hooks/useHttpRequestActions'; import { useHttpRequestActions } from '../../hooks/useHttpRequestActions';
import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace'; import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace';
import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest';
@@ -25,6 +26,7 @@ interface Props {
export function SidebarItemContextMenu({ child, show, close }: Props) { export function SidebarItemContextMenu({ child, show, close }: Props) {
const sendManyRequests = useSendManyRequests(); const sendManyRequests = useSendManyRequests();
const httpRequestActions = useHttpRequestActions(); const httpRequestActions = useHttpRequestActions();
const grpcRequestActions = useGrpcRequestActions();
const sendRequest = useSendAnyHttpRequest(); const sendRequest = useSendAnyHttpRequest();
const workspaces = useAtomValue(workspacesAtom); const workspaces = useAtomValue(workspacesAtom);
const moveToWorkspace = useMoveToWorkspace(child.id); const moveToWorkspace = useMoveToWorkspace(child.id);
@@ -65,25 +67,35 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
const requestItems: DropdownItem[] = const requestItems: DropdownItem[] =
child.model === 'http_request' child.model === 'http_request'
? [ ? [
{ {
label: 'Send', label: 'Send',
hotKeyAction: 'http_request.send', hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar hotKeyLabelOnly: true, // Already bound in URL bar
leftSlot: <Icon icon="send_horizontal" />, leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendRequest.mutate(child.id), onSelect: () => sendRequest.mutate(child.id),
},
...httpRequestActions.map((a) => ({
label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
onSelect: async () => {
const request = getModel('http_request', child.id);
if (request != null) await a.call(request);
}, },
...httpRequestActions.map((a) => ({ })),
label: a.label, { type: 'separator' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any ]
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />, : child.model === 'grpc_request'
onSelect: async () => { ? grpcRequestActions.map((a) => ({
const request = getModel('http_request', child.id); label: a.label,
if (request != null) await a.call(request); // eslint-disable-next-line @typescript-eslint/no-explicit-any
}, leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
})), onSelect: async () => {
{ type: 'separator' }, const request = getModel('grpc_request', child.id);
] if (request != null) await a.call(request);
: []; },
}))
: [];
return [ return [
...requestItems, ...requestItems,
{ {
@@ -134,6 +146,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
child.model, child.model,
createDropdownItems, createDropdownItems,
httpRequestActions, httpRequestActions,
grpcRequestActions,
moveToWorkspace.mutate, moveToWorkspace.mutate,
sendManyRequests, sendManyRequests,
sendRequest, sendRequest,

View File

@@ -1,3 +1,4 @@
import { getKeyValue } from '../lib/keyValueStore';
import { useKeyValue } from './useKeyValue'; import { useKeyValue } from './useKeyValue';
export function protoFilesArgs(requestId: string | null) { export function protoFilesArgs(requestId: string | null) {
@@ -10,3 +11,7 @@ export function protoFilesArgs(requestId: string | null) {
export function useGrpcProtoFiles(activeRequestId: string | null) { export function useGrpcProtoFiles(activeRequestId: string | null) {
return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] }); return useKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
} }
export async function getGrpcProtoFiles(activeRequestId: string | null) {
return getKeyValue<string[]>({ ...protoFilesArgs(activeRequestId), fallback: [] });
}

View File

@@ -0,0 +1,51 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp-internal/models';
import type {
CallGrpcRequestActionRequest,
GetGrpcRequestActionsResponse,
GrpcRequestAction,
} from '@yaakapp-internal/plugins';
import { useMemo } from 'react';
import { invokeCmd } from '../lib/tauri';
import { getGrpcProtoFiles } from './useGrpcProtoFiles';
import { usePluginsKey } from './usePlugins';
export type CallableGrpcRequestAction = Pick<GrpcRequestAction, 'label' | 'icon'> & {
call: (grpcRequest: GrpcRequest) => Promise<void>;
};
export function useGrpcRequestActions() {
const pluginsKey = usePluginsKey();
const actionsResult = useQuery<CallableGrpcRequestAction[]>({
queryKey: ['grpc_request_actions', pluginsKey],
queryFn: async () => {
const responses = await invokeCmd<GetGrpcRequestActionsResponse[]>(
'cmd_grpc_request_actions',
);
return responses.flatMap((r) =>
r.actions.map((a, i) => ({
label: a.label,
icon: a.icon,
call: async (grpcRequest: GrpcRequest) => {
const protoFiles = await getGrpcProtoFiles(grpcRequest.id);
const payload: CallGrpcRequestActionRequest = {
index: i,
pluginRefId: r.pluginRefId,
args: { grpcRequest, protoFiles },
};
await invokeCmd('cmd_call_grpc_request_action', { req: payload });
},
})),
);
},
});
const actions = useMemo(() => {
return actionsResult.data ?? [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(actionsResult.data)]);
return actions;
}

View File

@@ -4,6 +4,7 @@ import { invoke } from '@tauri-apps/api/core';
type TauriCmd = type TauriCmd =
| 'cmd_get_themes' | 'cmd_get_themes'
| 'cmd_call_http_authentication_action' | 'cmd_call_http_authentication_action'
| 'cmd_call_grpc_request_action'
| 'cmd_call_http_request_action' | 'cmd_call_http_request_action'
| 'cmd_check_for_updates' | 'cmd_check_for_updates'
| 'cmd_create_grpc_request' | 'cmd_create_grpc_request'
@@ -23,6 +24,7 @@ type TauriCmd =
| 'cmd_get_workspace_meta' | 'cmd_get_workspace_meta'
| 'cmd_grpc_go' | 'cmd_grpc_go'
| 'cmd_grpc_reflect' | 'cmd_grpc_reflect'
| 'cmd_grpc_request_actions'
| 'cmd_http_request_actions' | 'cmd_http_request_actions'
| 'cmd_import_data' | 'cmd_import_data'
| 'cmd_install_plugin' | 'cmd_install_plugin'