feat: opencollection actions

This commit is contained in:
Anoop M D
2025-12-18 21:19:29 +05:30
parent 678fa88a7c
commit 9738a2afb7
10 changed files with 141 additions and 48 deletions

16
package-lock.json generated
View File

@@ -26,6 +26,7 @@
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@opencollection/types": "0.3.0",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@stylistic/eslint-plugin": "^5.3.1",
@@ -5680,6 +5681,13 @@
"node": ">= 8"
}
},
"node_modules/@opencollection/types": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.3.0.tgz",
"integrity": "sha512-kw+co3sM4ATDQI85lgy5UmOilEHFVdNYvOjZXmnSw6PUDUTAGBiaZNdwdSXwp//o4IwKbTQb8/0I3UdjLKh+qA==",
"dev": true,
"license": "MIT"
},
"node_modules/@parcel/watcher": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz",
@@ -33626,7 +33634,6 @@
"devDependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@opencollection/types": "0.2.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
@@ -33646,13 +33653,6 @@
"typescript": "^4.8.4"
}
},
"packages/bruno-filestore/node_modules/@opencollection/types": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.2.0.tgz",
"integrity": "sha512-Lucjjoy+ZzfdjL0/9HF6PFlNSDG/m11VZBiR2K5XU6ChJ2XXfJyKocRB2g0tm7e5zQNMoVL3oUoDJ2gexx6xyg==",
"dev": true,
"license": "MIT"
},
"packages/bruno-filestore/node_modules/@rollup/plugin-typescript": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz",

View File

@@ -23,6 +23,7 @@
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@opencollection/types": "0.3.0",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@stylistic/eslint-plugin": "^5.3.1",

View File

@@ -22,8 +22,6 @@
"devDependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@opencollection/types": "0.2.0",
"@usebruno/schema-types": "0.0.1",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
@@ -31,6 +29,7 @@
"@types/jest": "^29.5.11",
"@types/lodash": "^4.14.191",
"@types/node": "^24.1.0",
"@usebruno/schema-types": "0.0.1",
"babel-jest": "^29.7.0",
"jest": "^29.2.0",
"nanoid": "3.3.8",

View File

@@ -0,0 +1,78 @@
import type { Action, ActionSetVariable, ActionVariableScope } from '@opencollection/types/common/actions';
import type { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';
import { uuid } from '../../../utils';
/**
* Convert Bruno post-response variables to OpenCollection actions.
* Post-response variables in Bruno are converted to 'set-variable' actions
* with phase 'after-response'.
*/
export const toOpenCollectionActions = (resVariables: BrunoVariables | null | undefined): Action[] | undefined => {
if (!resVariables?.length) {
return undefined;
}
const actions: Action[] = resVariables.map((v: BrunoVariable): ActionSetVariable => {
const action: ActionSetVariable = {
type: 'set-variable',
phase: 'after-response',
selector: {
expression: v.value || '',
method: 'jsonq'
},
variable: {
name: v.name || '',
scope: v.local ? 'request' : 'runtime' as ActionVariableScope
}
};
if (v.description?.trim().length) {
action.description = v.description;
}
if (v.enabled === false) {
action.disabled = true;
}
return action;
});
return actions.length > 0 ? actions : undefined;
};
/**
* Convert OpenCollection actions to Bruno post-response variables.
* Only 'set-variable' actions with phase 'after-response' are converted.
*/
export const toBrunoPostResponseVariables = (actions: Action[] | null | undefined): BrunoVariables => {
if (!actions?.length) {
return [];
}
const resVars: BrunoVariables = [];
actions.forEach((action: Action) => {
// Only process 'set-variable' actions with 'after-response' phase
if (action.type === 'set-variable' && action.phase === 'after-response') {
const setVarAction = action as ActionSetVariable;
const variable: BrunoVariable = {
uid: uuid(),
name: setVarAction.variable?.name || '',
value: setVarAction.selector?.expression || '',
enabled: setVarAction.disabled !== true,
local: false
};
if (setVarAction.description) {
variable.description = typeof setVarAction.description === 'string'
? setVarAction.description
: (setVarAction.description as any)?.content || '';
}
resVars.push(variable);
}
});
return resVars;
};

View File

@@ -3,36 +3,28 @@ import { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/coll
import { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';
import { uuid } from '../../../utils';
/**
* Convert Bruno pre-request variables to OpenCollection variables format.
* Note: Post-response variables are now converted to actions (see actions.ts).
*/
export const toOpenCollectionVariables = (variables: BrunoFolderRequest['vars'] | BrunoVariables | null | undefined): Variable[] | undefined => {
// Handle folder variables (has req/res structure)
// Handle folder variables (has req/res structure) - only use req vars
const hasReqRes = variables && 'req' in variables;
const reqVars = hasReqRes ? variables.req : variables as BrunoVariables;
const resVars = hasReqRes && 'res' in variables ? variables.res : [];
const reqVarsArray = Array.isArray(reqVars) ? reqVars : [];
const resVarsArray = Array.isArray(resVars) ? resVars : [];
const allVars = [...reqVarsArray, ...resVarsArray];
if (!allVars.length) {
if (!reqVarsArray.length) {
return undefined;
}
const ocVariables: Variable[] = allVars.map((v: BrunoVariable, index: number): Variable => {
const isResVar = index >= reqVarsArray.length;
const ocVariables: Variable[] = reqVarsArray.map((v: BrunoVariable): Variable => {
const variable: Variable = {
name: v.name || '',
value: v.value || ''
};
if (isResVar) {
const scopeMarker = '[post-response]';
if (v?.description?.trim().length) {
variable.description = `${scopeMarker} ${v.description}`;
} else {
variable.description = scopeMarker;
}
} else if (v?.description?.trim().length) {
if (v?.description?.trim().length) {
variable.description = v.description;
}
@@ -45,18 +37,18 @@ export const toOpenCollectionVariables = (variables: BrunoFolderRequest['vars']
return ocVariables.length > 0 ? ocVariables : undefined;
};
/**
* Convert OpenCollection variables to Bruno pre-request variables format.
* Note: Post-response variables come from actions (see actions.ts).
*/
export const toBrunoVariables = (variables: Variable[] | null | undefined): { req: BrunoVariables; res: BrunoVariables } => {
if (!variables?.length) {
return { req: [], res: [] };
}
const scopeMarker = '[post-response]';
const reqVars: BrunoVariables = [];
const resVars: BrunoVariables = [];
variables.forEach((v: Variable) => {
const isPostResponse = typeof v.description === 'string' && v.description.startsWith(scopeMarker);
const variable: BrunoVariable = {
uid: uuid(),
name: v.name || '',
@@ -65,19 +57,12 @@ export const toBrunoVariables = (variables: Variable[] | null | undefined): { re
local: false
};
if (isPostResponse) {
const cleanDesc = (v.description as string).substring(scopeMarker.length).trim();
if (cleanDesc) {
variable.description = cleanDesc;
}
resVars.push(variable);
} else {
if (v.description) {
variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
}
reqVars.push(variable);
if (v.description) {
variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
}
reqVars.push(variable);
});
return { req: reqVars, res: resVars };
return { req: reqVars, res: [] };
};

View File

@@ -5,6 +5,7 @@ import { toBrunoAuth } from '../common/auth';
import { toBrunoHttpHeaders } from '../common/headers';
import { toBrunoParams } from '../common/params';
import { toBrunoVariables } from '../common/variables';
import { toBrunoPostResponseVariables } from '../common/actions';
import { toBrunoScripts } from '../common/scripts';
import { toBrunoAssertions } from '../common/assertions';
import { uuid } from '../../../utils';
@@ -61,9 +62,13 @@ const parseGraphQLRequest = (ocRequest: GraphQLRequest): BrunoItem => {
brunoRequest.tests = scripts.tests;
}
// variables
// variables (pre-request from variables, post-response from actions)
const variables = toBrunoVariables(runtime?.variables);
brunoRequest.vars = variables;
const postResponseVars = toBrunoPostResponseVariables(runtime?.actions);
brunoRequest.vars = {
req: variables.req,
res: postResponseVars
};
// assertions
const assertions = toBrunoAssertions(runtime?.assertions);

View File

@@ -6,6 +6,7 @@ import { toBrunoHttpHeaders } from '../common/headers';
import { toBrunoParams } from '../common/params';
import { toBrunoBody } from '../common/body';
import { toBrunoVariables } from '../common/variables';
import { toBrunoPostResponseVariables } from '../common/actions';
import { toBrunoScripts } from '../common/scripts';
import { toBrunoAssertions } from '../common/assertions';
import { uuid } from '../../../utils';
@@ -59,9 +60,13 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
brunoRequest.tests = scripts.tests;
}
// variables
// variables (pre-request from variables, post-response from actions)
const variables = toBrunoVariables(runtime?.variables);
brunoRequest.vars = variables;
const postResponseVars = toBrunoPostResponseVariables(runtime?.actions);
brunoRequest.vars = {
req: variables.req,
res: postResponseVars
};
// assertions
const assertions = toBrunoAssertions(runtime?.assertions);

View File

@@ -5,6 +5,7 @@ import type { Auth } from '@opencollection/types/common/auth';
import type { Scripts } from '@opencollection/types/common/scripts';
import type { Variable } from '@opencollection/types/common/variables';
import type { Assertion } from '@opencollection/types/common/assertions';
import type { Action } from '@opencollection/types/common/actions';
import type { HttpRequestParam, HttpRequestHeader } from '@opencollection/types/requests/http';
import { stringifyYml } from '../utils';
import { isNonEmptyString, isNumber } from '../../../utils';
@@ -12,6 +13,7 @@ import { toOpenCollectionAuth } from '../common/auth';
import { toOpenCollectionHttpHeaders } from '../common/headers';
import { toOpenCollectionParams } from '../common/params';
import { toOpenCollectionVariables } from '../common/variables';
import { toOpenCollectionActions } from '../common/actions';
import { toOpenCollectionScripts } from '../common/scripts';
import { toOpenCollectionAssertions } from '../common/assertions';
@@ -98,6 +100,14 @@ const stringifyGraphQLRequest = (item: BrunoItem): string => {
hasRuntime = true;
}
// actions (from post-response variables)
const resVars = brunoRequest.vars?.res;
const actions: Action[] | undefined = toOpenCollectionActions(resVars);
if (actions) {
runtime.actions = actions;
hasRuntime = true;
}
// auth
const auth: Auth | undefined = toOpenCollectionAuth(brunoRequest.auth);
if (auth) {

View File

@@ -5,6 +5,7 @@ import type { Auth } from '@opencollection/types/common/auth';
import type { Scripts } from '@opencollection/types/common/scripts';
import type { Variable } from '@opencollection/types/common/variables';
import type { Assertion } from '@opencollection/types/common/assertions';
import type { Action } from '@opencollection/types/common/actions';
import type { HttpRequestParam, HttpRequestBody } from '@opencollection/types/requests/http';
import { stringifyYml } from '../utils';
import { toOpenCollectionAuth } from '../common/auth';
@@ -12,6 +13,7 @@ import { toOpenCollectionHttpHeaders, toOpenCollectionResponseHeaders } from '..
import { toOpenCollectionParams } from '../common/params';
import { toOpenCollectionBody } from '../common/body';
import { toOpenCollectionVariables } from '../common/variables';
import { toOpenCollectionActions } from '../common/actions';
import { toOpenCollectionScripts } from '../common/scripts';
import { toOpenCollectionAssertions } from '../common/assertions';
import { isNumber, isNonEmptyString } from '../../../utils';
@@ -85,6 +87,14 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
hasRuntime = true;
}
// actions (from post-response variables)
const resVars = brunoRequest.vars?.res;
const actions: Action[] | undefined = toOpenCollectionActions(resVars);
if (actions) {
runtime.actions = actions;
hasRuntime = true;
}
// auth
const auth: Auth | undefined = toOpenCollectionAuth(brunoRequest.auth);
if (auth) {

View File

@@ -22,8 +22,8 @@
"paths": {
"@usebruno/schema-types": ["packages/bruno-schema-types/dist/index.d.ts"],
"@usebruno/schema-types/*": ["packages/bruno-schema-types/dist/*"],
"@opencollection/types": ["packages/bruno-filestore/node_modules/@opencollection/types/dist/opencollection.d.ts"],
"@opencollection/types/*": ["packages/bruno-filestore/node_modules/@opencollection/types/dist/*"]
"@opencollection/types": ["node_modules/@opencollection/types/dist/opencollection.d.ts"],
"@opencollection/types/*": ["node_modules/@opencollection/types/dist/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.d.ts"],