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([
'**/node_modules/',
'**/dist/',
'**/build/',
'**/.eslintrc.cjs',
'**/.prettierrc.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-jwt",
"plugins/auth-oauth2",
"plugins/exporter-curl",
"plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/filter-jsonpath",
"plugins/filter-xpath",
"plugins/importer-curl",
@@ -85,6 +86,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"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 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 CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
@@ -336,14 +340,14 @@ export type GetCookieValueRequest = { name: string, };
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 GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
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 GetHttpRequestByIdRequest = { id: string, };
@@ -360,6 +364,8 @@ export type GetThemesRequest = Record<string, never>;
export type GetThemesResponse = { themes: Array<Theme>, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
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 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;
@@ -408,6 +414,10 @@ required?: boolean, };
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 RenderHttpRequestResponse = { httpRequest: HttpRequest, };

View File

@@ -9,6 +9,8 @@ import type {
OpenWindowRequest,
PromptTextRequest,
PromptTextResponse,
RenderGrpcRequestRequest,
RenderGrpcRequestResponse,
RenderHttpRequestRequest,
RenderHttpRequestResponse,
SendHttpRequestRequest,
@@ -45,6 +47,9 @@ export interface Context {
listNames(): Promise<ListCookieNamesResponse['names']>;
getValue(args: GetCookieValueRequest): Promise<GetCookieValueResponse['value']>;
};
grpcRequest: {
render(args: RenderGrpcRequestRequest): Promise<RenderGrpcRequestResponse['grpcRequest']>;
};
httpRequest: {
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
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 { AtLeast } from '../helpers';
import { AtLeast, MaybePromise } from '../helpers';
import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model';
@@ -21,5 +21,8 @@ export type ImportPluginResponse = null | {
export type ImporterPlugin = {
name: 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 type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { ImporterPlugin } from './ImporterPlugin';
import type { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
@@ -16,5 +17,6 @@ export type PluginDefinition = {
filter?: FilterPlugin;
authentication?: AuthenticationPlugin;
httpRequestActions?: HttpRequestActionPlugin[];
grpcRequestActions?: GrpcRequestActionPlugin[];
templateFunctions?: TemplateFunctionPlugin[];
};

View File

@@ -7,7 +7,7 @@ import {
GetCookieValueRequest,
GetCookieValueResponse,
GetHttpRequestByIdResponse,
GetKeyValueResponse,
GetKeyValueResponse, GrpcRequestAction,
HttpAuthenticationAction,
HttpRequestAction,
InternalEvent,
@@ -16,6 +16,7 @@ import {
PluginWindowContext,
PromptTextResponse,
RenderHttpRequestResponse,
RenderGrpcRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateFunctionArg,
@@ -145,6 +146,24 @@ export class PluginInstance {
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 (
payload.type === 'get_http_request_actions_request' &&
Array.isArray(this.#mod?.httpRequestActions)
@@ -208,13 +227,12 @@ export class PluginInstance {
if (payload.type === 'get_http_authentication_config_request' && this.#mod?.authentication) {
const { args, actions } = this.#mod.authentication;
const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
for (const v of args) {
if (v && 'dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
} else if (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 (
payload.type === 'call_template_function_request' &&
Array.isArray(this.#mod?.templateFunctions)
@@ -472,6 +502,19 @@ export class PluginInstance {
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: {
getById: async (args) => {
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
* 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, () => {
const stat = statSync(filepath);
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
cb(filepath);
const stat = statSync(filepath, { throwIfNoEntry: false });
if (stat == null || stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
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,
"version": "0.0.1",
"scripts": {
"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 ';
export const plugin: PluginDefinition = {
httpRequestActions: [{
label: 'Copy as Curl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: 'preview' });
const data = await convertToCurl(rendered_request);
await ctx.clipboard.copyText(data);
await ctx.toast.show({ message: 'Curl copied to clipboard', icon: 'copy', color: 'success' });
httpRequestActions: [
{
label: 'Copy as Curl',
icon: 'copy',
async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({
httpRequest: args.httpRequest,
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>) {
@@ -22,7 +31,6 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
if (request.method) xs.push('-X', request.method);
if (request.url) xs.push(quote(request.url));
xs.push(NEWLINE);
// Add URL params
@@ -51,7 +59,10 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
xs.push(NEWLINE);
}
} 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(NEWLINE);
} else if (typeof request.body?.text === 'string') {
@@ -84,7 +95,7 @@ export async function convertToCurl(request: Partial<HttpRequest>) {
}
function quote(arg: string): string {
const escaped = arg.replace(/'/g, '\\\'');
const escaped = arg.replace(/'/g, "\\'");
return `'${escaped}'`;
}
@@ -92,10 +103,10 @@ function onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {
return v.enabled !== false && !!v.name;
}
function maybeParseJSON(v: any, fallback: any): string {
function maybeParseJSON<T>(v: string, fallback: T) {
try {
return JSON.parse(v);
} catch (err) {
} catch {
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",
"scripts": {
"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",
"scripts": {
"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",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"jsonwebtoken": "^9.0.2"

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,8 @@
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.js",
"dev": "yaakcli dev ./src/index.js"
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"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 { 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 = {
importer: {
name: 'OpenAPI',
description: 'Import OpenAPI collections',
onImport(_ctx: Context, args: { text: string }) {
return convertOpenApi(args.text) as any;
return convertOpenApi(args.text);
},
},
};
export async function convertOpenApi(
contents: string,
): Promise<{ resources: ExportResources } | undefined> {
export async function convertOpenApi(contents: string): Promise<ImportPluginResponse | undefined> {
let postmanCollection;
try {
postmanCollection = await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
convert({ type: 'string', data: contents }, {}, (err, result: any) => {
if (err != null) reject(err);
@@ -35,7 +26,7 @@ export async function convertOpenApi(
}
});
});
} catch (err) {
} catch {
// Probably not an OpenAPI file, so skip it
return undefined;
}

View File

@@ -5,6 +5,7 @@
"main": "./build/index.js",
"scripts": {
"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,
Environment,
Folder,
HttpRequest,
HttpRequestHeader,
HttpUrlParameter,
PartialImportResources,
PluginDefinition,
Workspace,
} 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_0_0_SCHEMA = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json';
@@ -27,19 +29,19 @@ export const plugin: PluginDefinition = {
name: 'Postman',
description: 'Import postman collections',
onImport(_ctx: Context, args: { text: string }) {
return convertPostman(args.text) as any;
return convertPostman(args.text);
},
},
};
export function convertPostman(
contents: string,
): { resources: ExportResources } | undefined {
export function convertPostman(contents: string): ImportPluginResponse | undefined {
const root = parseJSONToRecord(contents);
if (root == null) return;
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)) {
return;
}
@@ -53,11 +55,17 @@ export function convertPostman(
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] = {
model: 'workspace',
id: generateId('workspace'),
name: info.name || 'Postman Import',
description: info.description?.content ?? info.description,
name: info.name ? String(info.name) : 'Postman Import',
description,
};
exportResources.workspaces.push(workspace);
@@ -68,14 +76,14 @@ export function convertPostman(
name: 'Global Variables',
workspaceId: workspace.id,
variables:
root.variable?.map((v: any) => ({
toArray<{ key: string; value: string }>(root.variable).map((v) => ({
name: v.key,
value: v.value,
})) ?? [],
};
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)) {
const folder: ExportResources['folders'][0] = {
model: 'folder',
@@ -94,7 +102,11 @@ export function convertPostman(
const requestAuthPath = importAuth(r.auth);
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 {
name: h.key,
value: h.value,
@@ -104,7 +116,9 @@ export function convertPostman(
// Add body headers only if they don't already exist
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) {
continue;
}
@@ -119,8 +133,8 @@ export function convertPostman(
workspaceId: workspace.id,
folderId,
name: v.name,
description: v.description || undefined,
method: r.method || 'GET',
description: v.description ? String(v.description) : undefined,
method: typeof r.method === 'string' ? r.method : 'GET',
url,
urlParameters,
body: bodyPatch.body,
@@ -139,17 +153,19 @@ export function convertPostman(
importItem(item);
}
const resources = deleteUndefinedAttrs(convertTemplateSyntax(exportResources));
const resources = deleteUndefinedAttrs(
convertTemplateSyntax(exportResources),
) as PartialImportResources;
return { resources };
}
function convertUrl(url: string | any): Pick<HttpRequest, 'url' | 'urlParameters'> {
if (typeof url === 'string') {
return { url, urlParameters: [] };
function convertUrl(rawUrl: string | unknown): Pick<HttpRequest, 'url' | 'urlParameters'> {
if (typeof rawUrl === 'string') {
return { url: rawUrl, urlParameters: [] };
}
url = toRecord(url);
const url = toRecord(rawUrl);
let v = '';
@@ -199,10 +215,8 @@ function convertUrl(url: string | any): Pick<HttpRequest, 'url' | 'urlParameters
return { url: v, urlParameters: params };
}
function importAuth(
rawAuth: any,
): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
const auth = toRecord(rawAuth);
function importAuth(rawAuth: unknown): Pick<HttpRequest, 'authentication' | 'authenticationType'> {
const auth = toRecord<{ username?: string; password?: string; token?: string }>(rawAuth);
if ('basic' in auth) {
return {
authenticationType: 'basic',
@@ -223,8 +237,22 @@ function importAuth(
}
}
function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
const body = toRecord(rawBody);
function importBody(rawBody: unknown): Pick<HttpRequest, 'body' | 'bodyType' | 'headers'> {
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') {
return {
headers: [
@@ -237,7 +265,10 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
bodyType: 'graphql',
body: {
text: JSON.stringify(
{ query: body.graphql.query, variables: parseJSONToRecord(body.graphql.variables) },
{
query: body.graphql?.query || '',
variables: parseJSONToRecord(body.graphql?.variables || '{}'),
},
null,
2,
),
@@ -254,7 +285,7 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
],
bodyType: 'application/x-www-form-urlencoded',
body: {
form: toArray(body.urlencoded).map((f) => ({
form: toArray<NonNullable<typeof body.urlencoded>[0]>(body.urlencoded).map((f) => ({
enabled: !f.disabled,
name: f.key ?? '',
value: f.value ?? '',
@@ -272,19 +303,19 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
],
bodyType: 'multipart/form-data',
body: {
form: toArray(body.formdata).map((f) =>
form: toArray<NonNullable<typeof body.formdata>[0]>(body.formdata).map((f) =>
f.src != null
? {
enabled: !f.disabled,
contentType: f.contentType ?? null,
name: f.key ?? '',
file: f.src ?? '',
}
enabled: !f.disabled,
contentType: f.contentType ?? null,
name: f.key ?? '',
file: f.src ?? '',
}
: {
enabled: !f.disabled,
name: f.key ?? '',
value: f.value ?? '',
},
enabled: !f.disabled,
name: f.key ?? '',
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 {
return toRecord(JSON.parse(jsonStr));
} catch (err) {
} catch {
return null;
}
return null;
}
function toRecord(value: any): Record<string, any> {
if (Object.prototype.toString.call(value) === '[object Object]') return value;
else return {};
function toRecord<T>(value: Record<string, T> | unknown): Record<string, T> {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, T>;
}
return {};
}
function toArray(value: any): any[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value;
function toArray<T>(value: unknown): T[] {
if (Object.prototype.toString.call(value) === '[object Array]') return value as T[];
else return [];
}

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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",
"scripts": {
"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",
"scripts": {
"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 = {
templateFunctions: [
@@ -7,7 +7,7 @@ export const plugin: PluginDefinition = {
description: 'Encode a value to base64',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
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',
args: [{ label: 'Encoded Value', type: 'text', name: 'value', multiLine: true }],
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)',
args: [{ label: 'Plain Text', type: 'text', name: 'value', multiLine: true }],
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 }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
try {
return decodeURIComponent(args.values.value ?? '');
return decodeURIComponent(String(args.values.value ?? ''));
} catch {
return '';
}

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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';
export const plugin: PluginDefinition = {
templateFunctions: [{
name: 'fs.readFile',
description: 'Read the contents of a file as utf-8',
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path) return null;
templateFunctions: [
{
name: 'fs.readFile',
description: 'Read the contents of a file as utf-8',
args: [{ title: 'Select File', type: 'file', name: 'path', label: 'File' }],
async onRender(_ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.path) return null;
try {
return fs.promises.readFile(args.values.path, 'utf-8');
} catch (err) {
return null;
}
try {
return fs.promises.readFile(String(args.values.path ?? ''), 'utf-8');
} catch {
return null;
}
},
},
}],
],
};

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
"dev": "yaakcli dev ./src/index.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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 = {
templateFunctions: [{
@@ -15,10 +15,10 @@ export const plugin: PluginDefinition = {
return await ctx.prompt.text({
id: `prompt-${args.values.label}`,
label: args.values.title ?? '',
title: args.values.title ?? '',
defaultValue: args.values.defaultValue,
placeholder: args.values.placeholder,
label: String(args.values.title ?? ''),
title: String(args.values.title ?? ''),
defaultValue: String(args.values.defaultValue),
placeholder: String(args.values.placeholder),
});
},
}],

View File

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

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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 = {
templateFunctions: [
{
name: 'request.body',
args: [{
name: 'requestId',
label: 'Http Request',
type: 'http_request',
}],
args: [
{
name: 'requestId',
label: 'Http Request',
type: 'http_request',
},
],
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;
return String(await ctx.templates.render({
data: httpRequest.body?.text ?? '',
purpose: args.purpose,
}));
return String(
await ctx.templates.render({
data: httpRequest.body?.text ?? '',
purpose: args.purpose,
}),
);
},
},
{
@@ -30,15 +35,22 @@ export const plugin: PluginDefinition = {
name: 'header',
label: 'Header Name',
type: 'text',
}],
},
],
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;
const header = httpRequest.headers.find(h => h.name.toLowerCase() === args.values.header?.toLowerCase());
return String(await ctx.templates.render({
data: header?.value ?? '',
purpose: args.purpose,
}));
const header = httpRequest.headers.find(
(h) => h.name.toLowerCase() === headerName.toLowerCase(),
);
return String(
await ctx.templates.render({
data: header?.value ?? '',
purpose: args.purpose,
}),
);
},
},
],

View File

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

View File

@@ -1,5 +1,5 @@
import { DOMParser } from '@xmldom/xmldom';
import {
import type {
CallTemplateFunctionArgs,
Context,
FormInput,
@@ -47,14 +47,14 @@ export const plugin: PluginDefinition = {
if (!args.values.request || !args.values.header) return null;
const response = await getResponse(ctx, {
requestId: args.values.request,
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ?? null,
behavior: args.values.behavior ? String(args.values.behavior) : null,
});
if (response == null) return null;
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;
},
@@ -77,9 +77,9 @@ export const plugin: PluginDefinition = {
if (!args.values.request || !args.values.path) return null;
const response = await getResponse(ctx, {
requestId: args.values.request,
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ?? null,
behavior: args.values.behavior ? String(args.values.behavior) : null,
});
if (response == null) return null;
@@ -90,19 +90,19 @@ export const plugin: PluginDefinition = {
let body;
try {
body = readFileSync(response.bodyPath, 'utf-8');
} catch (_) {
} catch {
return null;
}
try {
return filterJSONPath(body, args.values.path);
} catch (err) {
return filterJSONPath(body, String(args.values.path || ''));
} catch {
// Probably not JSON, try XPath
}
try {
return filterXPath(body, args.values.path);
} catch (err) {
return filterXPath(body, String(args.values.path || ''));
} catch {
// Probably not XML
}
@@ -113,17 +113,14 @@ export const plugin: PluginDefinition = {
name: 'response.body.raw',
description: 'Access the entire response body, as text',
aliases: ['response'],
args: [
requestArg,
behaviorArg,
],
args: [requestArg, behaviorArg],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request) return null;
const response = await getResponse(ctx, {
requestId: args.values.request,
requestId: String(args.values.request || ''),
purpose: args.purpose,
behavior: args.values.behavior ?? null,
behavior: args.values.behavior ? String(args.values.behavior) : null,
});
if (response == null) return null;
@@ -134,7 +131,7 @@ export const plugin: PluginDefinition = {
let body;
try {
body = readFileSync(response.bodyPath, 'utf-8');
} catch (_) {
} catch {
return null;
}
@@ -173,11 +170,18 @@ function filterXPath(body: string, path: string): string {
}
}
async function getResponse(ctx: Context, { requestId, behavior, purpose }: {
requestId: string,
behavior: string | null,
purpose: RenderPurpose,
}): Promise<HttpResponse | null> {
async function getResponse(
ctx: Context,
{
requestId,
behavior,
purpose,
}: {
requestId: string;
behavior: string | null;
purpose: RenderPurpose;
},
): Promise<HttpResponse | null> {
if (!requestId) return null;
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
// it as "smart" during preview.
let finalBehavior = (behavior === 'always' && purpose === 'preview')
? 'smart'
: behavior;
const finalBehavior = behavior === 'always' && purpose === 'preview' ? 'smart' : behavior;
// Send if no responses and "smart," or "always"
if ((finalBehavior === 'smart' && response == null) || finalBehavior === 'always') {

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
"version": "0.0.1",
"scripts": {
"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::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
use yaak_plugins::events::{
CallHttpRequestActionRequest, FilterResponse, GetHttpAuthenticationConfigResponse,
CallGrpcRequestActionRequest, CallHttpRequestActionRequest, FilterResponse,
GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse,
GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse,
GetTemplateFunctionsResponse, InternalEvent, InternalEventPayload, JsonPrimitive,
PluginWindowContext, RenderPurpose,
@@ -791,6 +792,14 @@ async fn cmd_http_request_actions<R: Runtime>(
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]
async fn cmd_template_functions<R: Runtime>(
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?)
}
#[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]
async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>,
@@ -1220,6 +1238,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action,
cmd_call_http_request_action,
cmd_call_grpc_request_action,
cmd_check_for_updates,
cmd_create_grpc_request,
cmd_curl_to_request,
@@ -1236,6 +1255,7 @@ pub fn run() {
cmd_get_workspace_meta,
cmd_grpc_go,
cmd_grpc_reflect,
cmd_grpc_request_actions,
cmd_http_request_actions,
cmd_import_data,
cmd_install_plugin,

View File

@@ -1,6 +1,6 @@
use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value};
use crate::window::{CreateWindowConfig, create_window};
use crate::render::{render_grpc_request, render_http_request, render_json_value};
use crate::window::{create_window, CreateWindowConfig};
use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window,
@@ -13,13 +13,7 @@ use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::UpdateSource;
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
ListCookieNamesResponse, PluginWindowContext, RenderHttpRequestResponse,
SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse,
WindowNavigateEvent,
};
use yaak_plugins::events::{Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload, ListCookieNamesResponse, PluginWindowContext, RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowNavigateEvent};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -68,6 +62,30 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
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) => {
let window = get_window_from_window_context(app_handle, &window_context)
.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 {
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());
} else {
continue;
}
@@ -62,6 +67,8 @@ pub async fn fill_pool_from_files(
} else {
debug!("ignoring {:?} since it does not exist.", parent)
}
args.push(p.to_string_lossy().to_string());
}
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 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 CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
@@ -336,14 +340,14 @@ export type GetCookieValueRequest = { name: string, };
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 GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
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 GetHttpRequestByIdRequest = { id: string, };
@@ -360,6 +364,8 @@ export type GetThemesRequest = Record<string, never>;
export type GetThemesResponse = { themes: Array<Theme>, };
export type GrpcRequestAction = { label: string, icon?: Icon, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
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 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;
@@ -408,6 +414,10 @@ required?: boolean, };
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 RenderHttpRequestResponse = { httpRequest: HttpRequest, };

View File

@@ -89,11 +89,16 @@ pub enum InternalEventPayload {
GetCookieValueRequest(GetCookieValueRequest),
GetCookieValueResponse(GetCookieValueResponse),
// Request Actions
// HTTP Request Actions
GetHttpRequestActionsRequest(EmptyPayload),
GetHttpRequestActionsResponse(GetHttpRequestActionsResponse),
CallHttpRequestActionRequest(CallHttpRequestActionRequest),
// Grpc Request Actions
GetGrpcRequestActionsRequest(EmptyPayload),
GetGrpcRequestActionsResponse(GetGrpcRequestActionsResponse),
CallGrpcRequestActionRequest(CallGrpcRequestActionRequest),
// Template Functions
GetTemplateFunctionsRequest,
GetTemplateFunctionsResponse(GetTemplateFunctionsResponse),
@@ -116,6 +121,9 @@ pub enum InternalEventPayload {
RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse),
RenderGrpcRequestRequest(RenderGrpcRequestRequest),
RenderGrpcRequestResponse(RenderGrpcRequestResponse),
GetKeyValueRequest(GetKeyValueRequest),
GetKeyValueResponse(GetKeyValueResponse),
SetKeyValueRequest(SetKeyValueRequest),
@@ -287,6 +295,21 @@ pub struct RenderHttpRequestResponse {
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)]
#[serde(default, rename_all = "camelCase")]
#[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)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
@@ -1005,6 +1023,40 @@ pub struct CallHttpRequestActionArgs {
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)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]

View File

@@ -3,10 +3,11 @@ use crate::error::Error::{
};
use crate::error::Result;
use crate::events::{
BootRequest, CallHttpAuthenticationActionArgs, CallHttpAuthenticationActionRequest,
CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, CallHttpRequestActionRequest,
CallTemplateFunctionArgs, CallTemplateFunctionRequest, CallTemplateFunctionResponse,
EmptyPayload, FilterRequest, FilterResponse, GetHttpAuthenticationConfigRequest,
BootRequest, CallGrpcRequestActionRequest, CallHttpAuthenticationActionArgs,
CallHttpAuthenticationActionRequest, CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, EmptyPayload, FilterRequest,
FilterResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, GetThemesRequest,
GetThemesResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
@@ -426,6 +427,27 @@ impl PluginManager {
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>(
&self,
window: &WebviewWindow<R>,
@@ -495,6 +517,23 @@ impl PluginManager {
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>(
&self,
window: &WebviewWindow<R>,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import { getKeyValue } from '../lib/keyValueStore';
import { useKeyValue } from './useKeyValue';
export function protoFilesArgs(requestId: string | null) {
@@ -10,3 +11,7 @@ export function protoFilesArgs(requestId: string | null) {
export function useGrpcProtoFiles(activeRequestId: string | null) {
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 =
| 'cmd_get_themes'
| 'cmd_call_http_authentication_action'
| 'cmd_call_grpc_request_action'
| 'cmd_call_http_request_action'
| 'cmd_check_for_updates'
| 'cmd_create_grpc_request'
@@ -23,6 +24,7 @@ type TauriCmd =
| 'cmd_get_workspace_meta'
| 'cmd_grpc_go'
| 'cmd_grpc_reflect'
| 'cmd_grpc_request_actions'
| 'cmd_http_request_actions'
| 'cmd_import_data'
| 'cmd_install_plugin'