diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index f21feb1c152..b44ec4ff8a0 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1079,11 +1079,12 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || []; - const newQueryParams = map(params, ({ uid, name = '', value = '', description = '', type = 'query', enabled = true }) => ({ + const newQueryParams = map(params, ({ uid, name = '', value = '', description = '', annotations = null, type = 'query', enabled = true }) => ({ uid: uid || uuid(), name, value, description, + annotations, type, enabled })); @@ -1325,11 +1326,12 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.headers = map(action.payload.headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + item.draft.request.headers = map(action.payload.headers, ({ uid, name = '', value = '', description = '', annotations = null, enabled = true }) => ({ uid: uid || uuid(), name, value, description, + annotations, enabled })); }, @@ -1353,11 +1355,12 @@ export const collectionsSlice = createSlice({ collection.draft.root.request = {}; } - collection.draft.root.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + collection.draft.root.request.headers = map(headers, ({ uid, name = '', value = '', description = '', annotations = null, enabled = true }) => ({ uid: uid || uuid(), name, value, description, + annotations, enabled })); }, @@ -1380,11 +1383,12 @@ export const collectionsSlice = createSlice({ if (!folder.draft.request) { folder.draft.request = {}; } - folder.draft.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({ + folder.draft.request.headers = map(headers, ({ uid, name = '', value = '', description = '', annotations = null, enabled = true }) => ({ uid: uid || uuid(), name, value, description, + annotations, enabled })); }, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index c3416e60056..b6e631202fe 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -181,6 +181,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} name: header.name, value: header.value, description: header.description, + annotations: header.annotations, enabled: header.enabled }; }); @@ -193,6 +194,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} name: param.name, value: param.value, description: param.description, + annotations: param.annotations, type: param.type, enabled: param.enabled }; @@ -745,6 +747,7 @@ export const transformRequestToSaveToFilesystem = (item) => { name: param.name, value: param.value, description: param.description, + annotations: param.annotations, type: param.type, enabled: param.enabled }); @@ -757,6 +760,7 @@ export const transformRequestToSaveToFilesystem = (item) => { name: header.name, value: header.value, description: header.description, + annotations: header.annotations, enabled: header.enabled }); }); @@ -813,6 +817,7 @@ export const transformCollectionRootToSave = (collection) => { name: header.name, value: header.value, description: header.description, + annotations: header.annotations, enabled: header.enabled }); }); @@ -843,6 +848,7 @@ export const transformFolderRootToSave = (folder) => { name: header.name, value: header.value, description: header.description, + annotations: header.annotations, enabled: header.enabled }); }); diff --git a/packages/bruno-app/src/utils/collections/index.spec.js b/packages/bruno-app/src/utils/collections/index.spec.js index 7ff987b1e60..7297a43b56b 100644 --- a/packages/bruno-app/src/utils/collections/index.spec.js +++ b/packages/bruno-app/src/utils/collections/index.spec.js @@ -1,5 +1,5 @@ const { describe, it, expect } = require('@jest/globals'); -import { mergeHeaders } from './index'; +import { mergeHeaders, transformRequestToSaveToFilesystem } from './index'; describe('mergeHeaders', () => { it('should include headers from collection, folder and request (with correct precedence)', () => { @@ -35,3 +35,54 @@ describe('mergeHeaders', () => { expect(names).toEqual(expect.arrayContaining(['X-Collection', 'X-Folder', 'X-Request'])); }); }); + +describe('transformRequestToSaveToFilesystem', () => { + it('preserves header and param annotations', () => { + const item = { + uid: 'requestuid123456789012', + type: 'http-request', + name: 'Annotated Request', + seq: 1, + settings: {}, + tags: [], + examples: [], + request: { + method: 'GET', + url: 'https://example.com', + params: [ + { + uid: 'paramuid1234567890123', + name: 'q', + value: '1', + description: '', + annotations: [{ name: 'param-note', value: 'keep me' }], + type: 'query', + enabled: true + } + ], + headers: [ + { + uid: 'headeruid123456789012', + name: 'X-Test', + value: '1', + description: '', + annotations: [{ name: 'header-note', value: 'keep me' }], + enabled: true + } + ], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: { req: '', res: '' }, + vars: { req: [], res: [] }, + assertions: [], + tests: '', + docs: '' + } + }; + + const transformed = transformRequestToSaveToFilesystem(item); + + expect(transformed.request.params[0].annotations).toEqual([{ name: 'param-note', value: 'keep me' }]); + expect(transformed.request.headers[0].annotations).toEqual([{ name: 'header-note', value: 'keep me' }]); + }); +}); diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 5ea2d49af10..9579717f86f 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -645,6 +645,7 @@ const transformRequestToSaveToFilesystem = (item) => { name: param.name, value: param.value, description: param.description, + annotations: param.annotations, type: param.type, enabled: param.enabled }); @@ -657,6 +658,7 @@ const transformRequestToSaveToFilesystem = (item) => { name: header.name, value: header.value, description: header.description, + annotations: header.annotations, enabled: header.enabled }); }); diff --git a/packages/bruno-electron/src/utils/tests/collection-utils.spec.js b/packages/bruno-electron/src/utils/tests/collection-utils.spec.js index ad92e0ba605..d4552bea303 100644 --- a/packages/bruno-electron/src/utils/tests/collection-utils.spec.js +++ b/packages/bruno-electron/src/utils/tests/collection-utils.spec.js @@ -20,6 +20,7 @@ describe('transformRequestToSaveToFilesystem', () => { name: 'param1', value: 'value1', description: 'Test parameter', + annotations: [{ name: 'note', value: 'param annotation' }], type: 'text', enabled: true } @@ -30,6 +31,7 @@ describe('transformRequestToSaveToFilesystem', () => { name: 'Content-Type', value: 'application/json', description: 'Request content type', + annotations: [{ name: 'note', value: 'header annotation' }], enabled: true } ], @@ -101,6 +103,7 @@ describe('transformRequestToSaveToFilesystem', () => { name: 'param1', value: 'value1', description: 'Test parameter', + annotations: [{ name: 'note', value: 'param annotation' }], type: 'text', enabled: true }); @@ -112,6 +115,7 @@ describe('transformRequestToSaveToFilesystem', () => { name: 'Content-Type', value: 'application/json', description: 'Request content type', + annotations: [{ name: 'note', value: 'header annotation' }], enabled: true }); }); diff --git a/packages/bruno-schema-types/src/collection/environment.ts b/packages/bruno-schema-types/src/collection/environment.ts index ecc14b7d1c4..2ba57bb27d4 100644 --- a/packages/bruno-schema-types/src/collection/environment.ts +++ b/packages/bruno-schema-types/src/collection/environment.ts @@ -1,4 +1,4 @@ -import type { UID } from '../common'; +import type { UID, Annotation } from '../common'; export interface EnvironmentVariable { uid: UID; @@ -7,6 +7,7 @@ export interface EnvironmentVariable { type: 'text'; enabled?: boolean; secret?: boolean; + annotations?: Annotation[] | null; } export interface Environment { diff --git a/packages/bruno-schema-types/src/common/annotation.ts b/packages/bruno-schema-types/src/common/annotation.ts new file mode 100644 index 00000000000..aac9cfb772d --- /dev/null +++ b/packages/bruno-schema-types/src/common/annotation.ts @@ -0,0 +1,7 @@ +/** + * Annotation applied to pairs (headers, vars, params, etc.) + */ +export interface Annotation { + name: string; + value?: string | null; +} diff --git a/packages/bruno-schema-types/src/common/index.ts b/packages/bruno-schema-types/src/common/index.ts index d19826f5934..c50cd3eeca0 100644 --- a/packages/bruno-schema-types/src/common/index.ts +++ b/packages/bruno-schema-types/src/common/index.ts @@ -1,6 +1,7 @@ export type { UID } from './uid'; export type { KeyValue } from './key-value'; export type { Variable, Variables } from './variables'; +export type { Annotation } from './annotation'; export type { MultipartFormEntry, MultipartForm } from './multipart-form'; export type { FileEntry, FileList } from './file'; export type { GraphqlBody } from './graphql'; diff --git a/packages/bruno-schema-types/src/common/key-value.ts b/packages/bruno-schema-types/src/common/key-value.ts index 8007393f280..a3cf4d04d93 100644 --- a/packages/bruno-schema-types/src/common/key-value.ts +++ b/packages/bruno-schema-types/src/common/key-value.ts @@ -1,3 +1,4 @@ +import { Annotation } from './annotation'; import type { UID } from './uid'; /** @@ -9,4 +10,5 @@ export interface KeyValue { value?: string | null; description?: string | null; enabled?: boolean; + annotations?: Annotation[] | null; } diff --git a/packages/bruno-schema-types/src/common/variables.ts b/packages/bruno-schema-types/src/common/variables.ts index 8d45d14012c..a96f4510b0a 100644 --- a/packages/bruno-schema-types/src/common/variables.ts +++ b/packages/bruno-schema-types/src/common/variables.ts @@ -1,3 +1,4 @@ +import { Annotation } from './annotation'; import type { UID } from './uid'; /** @@ -10,6 +11,7 @@ export interface Variable { description?: string | null; enabled?: boolean; local?: boolean; + annotations?: Annotation[] | null; } export type Variables = Variable[] | null; diff --git a/packages/bruno-schema/src/collections/annotationsSchema.spec.js b/packages/bruno-schema/src/collections/annotationsSchema.spec.js new file mode 100644 index 00000000000..6c83c09f964 --- /dev/null +++ b/packages/bruno-schema/src/collections/annotationsSchema.spec.js @@ -0,0 +1,53 @@ +const { itemSchema, environmentSchema, collectionSchema } = require('./index'); + +describe('annotation acceptance', () => { + test('itemSchema accepts annotations on headers and params', async () => { + const item = { + uid: 'aaaaaaaaaaaaaaaaaaaaa', + type: 'http-request', + name: 'Req', + request: { + url: 'https://example.com', + method: 'GET', + headers: [ + { uid: 'bbbbbbbbbbbbbbbbbbbbb', name: 'X-Test', value: '1', annotations: [{ name: 'note', value: 'header note' }] } + ], + params: [ + { uid: 'ccccccccccccccccccccc', name: 'q', value: '1', type: 'query', annotations: [{ name: 'hint' }] } + ], + }, + }; + + await expect(itemSchema.validate(item)).resolves.toBeTruthy(); + }); + + test('environmentSchema accepts annotations on variables', async () => { + const env = { + uid: 'ddddddddddddddddddddd', + name: 'Env', + variables: [ + { uid: 'eeeeeeeeeeeeeeeeeeeee', name: 'API_KEY', value: 'abc', annotations: [{ name: 'secret', value: null }], type: 'text', enabled: true, secret: false } + ] + }; + + await expect(environmentSchema.validate(env)).resolves.toBeTruthy(); + }); + + test('collectionSchema accepts annotations in item vars and items', async () => { + const coll = { + version: '1', + uid: 'fffffffffffffffffffff', + name: 'Coll', + items: [ + { + uid: 'ggggggggggggggggggggg', + type: 'http-request', + name: 'Req2', + request: { url: '/path', method: 'POST', headers: [], params: [], vars: { req: [{ uid: 'hhhhhhhhhhhhhhhhhhhhh', name: 'base', value: 'https://example.com', annotations: [{ name: 'base-note' }] }] } } + } + ] + }; + + await expect(collectionSchema.validate(coll)).resolves.toBeTruthy(); + }); +}); diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b618af6f668..bbb5491631f 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -1,11 +1,22 @@ const Yup = require('yup'); const { uidSchema } = require('../common'); +const annotationSchema = Yup.object({ + name: Yup.string().min(1).required('annotation name is required'), + value: Yup.string().nullable() +}).noUnknown(true) + .strict(); + const environmentVariablesSchema = Yup.object({ uid: uidSchema, name: Yup.string().nullable(), // Allow mixed types (string, number, boolean, object) to support setting non-string values via scripts. value: Yup.mixed().nullable(), + annotations: Yup.array() + .of( + annotationSchema + ) + .nullable(), type: Yup.string().oneOf(['text']).required('type is required'), enabled: Yup.boolean().defined(), secret: Yup.boolean() @@ -29,6 +40,11 @@ const keyValueSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), description: Yup.string().nullable(), + annotations: Yup.array() + .of( + annotationSchema + ) + .nullable(), enabled: Yup.boolean() }) .noUnknown(true) @@ -79,6 +95,12 @@ const varsSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), description: Yup.string().nullable(), + // Optional annotations on variables + annotations: Yup.array() + .of( + annotationSchema + ) + .nullable(), enabled: Yup.boolean(), // todo @@ -109,6 +131,17 @@ const multipartFormSchema = Yup.object({ then: Yup.array().of(Yup.string().nullable()).nullable(), otherwise: Yup.string().nullable() }), + // Optional annotations on multipart entries + annotations: Yup.array() + .of( + Yup.object({ + name: Yup.string().min(1).required('annotation name is required'), + value: Yup.string().nullable() + }) + .noUnknown(true) + .strict() + ) + .nullable(), description: Yup.string().nullable(), contentType: Yup.string().nullable(), enabled: Yup.boolean() @@ -126,6 +159,16 @@ const fileSchema = Yup.object({ .noUnknown(true) .strict(); +// Add annotations to file entries (when parsed from body:file blocks they can have @contentType only currently, +// but adding annotations ensures roundtrip validation doesn't fail if annotations are present in future) +const fileSchemaWithAnnotations = fileSchema.shape({ + annotations: Yup.array() + .of( + annotationSchema + ) + .nullable() +}); + const requestBodySchema = Yup.object({ mode: Yup.string() .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file']) @@ -137,7 +180,7 @@ const requestBodySchema = Yup.object({ formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(multipartFormSchema).nullable(), graphql: graphqlBodySchema.nullable(), - file: Yup.array().of(fileSchema).nullable() + file: Yup.array().of(fileSchemaWithAnnotations).nullable() }) .noUnknown(true) .strict(); @@ -378,6 +421,12 @@ const requestParamsSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), description: Yup.string().nullable(), + // Optional annotations on params + annotations: Yup.array() + .of( + annotationSchema + ) + .nullable(), type: Yup.string().oneOf(['query', 'path']).required('type is required'), enabled: Yup.boolean() }) @@ -649,5 +698,6 @@ module.exports = { itemSchema, environmentSchema, environmentsSchema, - collectionSchema + collectionSchema, + annotationSchema };