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 f21feb1c1..b44ec4ff8 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 c3416e600..b6e631202 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 7ff987b1e..7297a43b5 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 5ea2d49af..9579717f8 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 ad92e0ba6..d4552bea3 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-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 1e1d5f0da..0cce61d1f 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -585,10 +585,11 @@ ${indentString(body.sparql)} const selected = item.selected ? '' : '~'; const contentType = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; + const annotPrefix = serializeAnnotations(item.annotations); const filePath = item.filePath || ''; const value = `@file(${filePath})`; const itemName = 'file'; - return `${selected}${itemName}: ${value}${contentType}`; + return `${annotPrefix}${selected}${itemName}: ${value}${contentType}`; }) .join('\n') )}`; diff --git a/packages/bruno-lang/v2/tests/annotations.spec.js b/packages/bruno-lang/v2/tests/annotations.spec.js index 44f5655af..e9907aa97 100644 --- a/packages/bruno-lang/v2/tests/annotations.spec.js +++ b/packages/bruno-lang/v2/tests/annotations.spec.js @@ -320,6 +320,35 @@ headers { expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: '{{baseUrl}}/path' }]); }); + it('serializeAnnotations — annotation on params:path', () => { + const json = { + params: [{ name: 'userId', value: '123', enabled: true, type: 'path', annotations: [{ name: 'description', value: 'user id' }] }] + }; + const bru = jsonToBru(json); + expect(bru).toContain('params:path {'); + expect(bru).toContain('@description(\'user id\')\n userId: 123'); + }); + + it('serializeAnnotations — annotation on metadata', () => { + const json = { + metadata: [{ name: 'trace-id', value: 'abc123', enabled: true, annotations: [{ name: 'description', value: 'trace id' }] }] + }; + const bru = jsonToBru(json); + expect(bru).toContain('metadata {'); + expect(bru).toContain('@description(\'trace id\')\n trace-id: abc123'); + }); + + it('serializeAnnotations — annotation on body:form-urlencoded', () => { + const json = { + body: { + formUrlEncoded: [{ name: 'username', value: 'alice', enabled: true, annotations: [{ name: 'description', value: 'username field' }] }] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('body:form-urlencoded {'); + expect(bru).toContain('@description(\'username field\')\n username: alice'); + }); + it('annotation on params:query block', () => { const input = ` params:query { @@ -333,6 +362,45 @@ params:query { ]); }); + it('annotation on params:path block', () => { + const input = ` +params:path { + @description('user id') + userId: 123 +} +`; + const output = parser(input); + expect(output.params).toEqual([ + { name: 'userId', value: '123', enabled: true, type: 'path', annotations: [{ name: 'description', value: 'user id' }] } + ]); + }); + + it('annotation on metadata block', () => { + const input = ` +metadata { + @description('trace id') + trace-id: abc123 +} +`; + const output = parser(input); + expect(output.metadata).toEqual([ + { name: 'trace-id', value: 'abc123', enabled: true, annotations: [{ name: 'description', value: 'trace id' }] } + ]); + }); + + it('annotation on body:form-urlencoded block', () => { + const input = ` +body:form-urlencoded { + @description('username field') + username: alice +} +`; + const output = parser(input); + expect(output.body.formUrlEncoded).toEqual([ + { name: 'username', value: 'alice', enabled: true, annotations: [{ name: 'description', value: 'username field' }] } + ]); + }); + it('annotation on vars:pre-request block', () => { const input = ` vars:pre-request { @@ -352,6 +420,225 @@ vars:pre-request { ]); }); + it('annotation on vars:post-response block', () => { + const input = ` +vars:post-response { + @description('auth token') + token: abc123 +} +`; + const output = parser(input); + expect(output.vars.res).toEqual([ + { + name: 'token', + value: 'abc123', + enabled: true, + local: false, + annotations: [{ name: 'description', value: 'auth token' }] + } + ]); + }); + + it('annotation on local vars:pre-request pair', () => { + const input = ` +vars:pre-request { + @description('local base url') + @BASE_URL: http://localhost +} +`; + const output = parser(input); + expect(output.vars.req).toEqual([ + { + name: 'BASE_URL', + value: 'http://localhost', + enabled: true, + local: true, + annotations: [{ name: 'description', value: 'local base url' }] + } + ]); + }); + + it('annotation on local vars:post-response pair', () => { + const input = ` +vars:post-response { + @description('local token') + @token: abc123 +} +`; + const output = parser(input); + expect(output.vars.res).toEqual([ + { + name: 'token', + value: 'abc123', + enabled: true, + local: true, + annotations: [{ name: 'description', value: 'local token' }] + } + ]); + }); + + it('annotation on body:multipart-form text field', () => { + const input = ` +body:multipart-form { + @description('plain field') + field: value @contentType(text/plain) +} +`; + const output = parser(input); + expect(output.body.multipartForm).toEqual([ + { + name: 'field', + value: 'value', + enabled: true, + type: 'text', + contentType: 'text/plain', + annotations: [{ name: 'description', value: 'plain field' }] + } + ]); + }); + + it('annotation on body:multipart-form file field', () => { + const input = ` +body:multipart-form { + @description('upload image') + upload: @file(/tmp/a.png|/tmp/b.png) @contentType(image/png) +} +`; + const output = parser(input); + expect(output.body.multipartForm).toEqual([ + { + name: 'upload', + value: ['/tmp/a.png', '/tmp/b.png'], + enabled: true, + type: 'file', + contentType: 'image/png', + annotations: [{ name: 'description', value: 'upload image' }] + } + ]); + }); + + it('annotation on body:file', () => { + const input = ` +body:file { + @description('upload doc') + file: @file(/tmp/readme.pdf) @contentType(application/pdf) +} +`; + const output = parser(input); + expect(output.body.file).toEqual([ + { + filePath: '/tmp/readme.pdf', + selected: true, + contentType: 'application/pdf', + annotations: [{ name: 'description', value: 'upload doc' }] + } + ]); + }); + + it('serializeAnnotations — multipart text field with contentType', () => { + const json = { + body: { + multipartForm: [ + { + name: 'field', + value: 'value', + enabled: true, + type: 'text', + contentType: 'text/plain', + annotations: [{ name: 'description', value: 'plain field' }] + } + ] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('@description(\'plain field\')\n field: value @contentType(text/plain)'); + }); + + it('serializeAnnotations — multipart file field with contentType', () => { + const json = { + body: { + multipartForm: [ + { + name: 'upload', + value: ['/tmp/a.png', '/tmp/b.png'], + enabled: true, + type: 'file', + contentType: 'image/png', + annotations: [{ name: 'description', value: 'upload image' }] + } + ] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('@description(\'upload image\')\n upload: @file(/tmp/a.png|/tmp/b.png) @contentType(image/png)'); + }); + + it('serializeAnnotations — annotation on vars:post-response', () => { + const json = { + vars: { + res: [{ name: 'token', value: 'abc123', enabled: true, local: false, annotations: [{ name: 'description', value: 'auth token' }] }] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('vars:post-response {'); + expect(bru).toContain('@description(\'auth token\')\n token: abc123'); + }); + + it('serializeAnnotations — annotation on local vars:pre-request', () => { + const json = { + vars: { + req: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: true, annotations: [{ name: 'description', value: 'local base url' }] }] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('vars:pre-request {'); + expect(bru).toContain('@description(\'local base url\')\n @BASE_URL: http://localhost'); + }); + + it('serializeAnnotations — annotation on disabled local vars:post-response', () => { + const json = { + vars: { + res: [{ name: 'token', value: 'abc123', enabled: false, local: true, annotations: [{ name: 'description', value: 'local token' }] }] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('vars:post-response {'); + expect(bru).toContain('@description(\'local token\')\n ~@token: abc123'); + }); + + it('serializeAnnotations — body:file with annotations', () => { + const json = { + body: { + file: [{ filePath: '/tmp/readme.pdf', selected: true, contentType: 'application/pdf', annotations: [{ name: 'description', value: 'upload doc' }] }] + } + }; + const bru = jsonToBru(json); + expect(bru).toContain('body:file {'); + expect(bru).toContain('@description(\'upload doc\')\n file: @file(/tmp/readme.pdf) @contentType(application/pdf)'); + const parsed = parser(bru); + expect(parsed.body.file).toEqual(json.body.file); + }); + + it('roundtrip — multipart annotation survives json→bru→json', () => { + const json = { + body: { + multipartForm: [ + { + name: 'upload', + value: ['/tmp/a.png'], + enabled: true, + type: 'file', + contentType: 'image/png', + annotations: [{ name: 'description', value: 'upload image' }] + } + ] + } + }; + const bru = jsonToBru(json); + const parsed = parser(bru); + expect(parsed.body.multipartForm).toEqual(json.body.multipartForm); + }); + it('roundtrip: bru → json → bru → json equal', () => { const input = `get { url: https://example.com @@ -792,6 +1079,39 @@ describe('collection pair annotations', () => { expect(bru).toContain('@description(\'base url\')\n BASE_URL: http://localhost'); }); + it('serializeAnnotations in jsonToCollectionBru — vars:post-response with annotation', () => { + const json = { + vars: { + res: [{ name: 'token', value: 'abc123', enabled: true, local: false, annotations: [{ name: 'description', value: 'auth token' }] }] + } + }; + const bru = jsonToCollectionBru(json); + expect(bru).toContain('vars:post-response {'); + expect(bru).toContain('@description(\'auth token\')\n token: abc123'); + }); + + it('serializeAnnotations in jsonToCollectionBru — local vars:pre-request with annotation', () => { + const json = { + vars: { + req: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: true, annotations: [{ name: 'description', value: 'local base url' }] }] + } + }; + const bru = jsonToCollectionBru(json); + expect(bru).toContain('vars:pre-request {'); + expect(bru).toContain('@description(\'local base url\')\n @BASE_URL: http://localhost'); + }); + + it('serializeAnnotations in jsonToCollectionBru — disabled local vars:post-response with annotation', () => { + const json = { + vars: { + res: [{ name: 'token', value: 'abc123', enabled: false, local: true, annotations: [{ name: 'description', value: 'local token' }] }] + } + }; + const bru = jsonToCollectionBru(json); + expect(bru).toContain('vars:post-response {'); + expect(bru).toContain('@description(\'local token\')\n ~@token: abc123'); + }); + it('parseAndSerialise - bru sourced roundtrip check - collection headers', () => { const input = `headers { @description('content type') diff --git a/packages/bruno-schema-types/src/collection/environment.ts b/packages/bruno-schema-types/src/collection/environment.ts index ecc14b7d1..2ba57bb27 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 000000000..aac9cfb77 --- /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/file.ts b/packages/bruno-schema-types/src/common/file.ts index 1f95c7e24..6c73e64c9 100644 --- a/packages/bruno-schema-types/src/common/file.ts +++ b/packages/bruno-schema-types/src/common/file.ts @@ -1,3 +1,4 @@ +import { Annotation } from './annotation'; import type { UID } from './uid'; export interface FileEntry { @@ -5,6 +6,7 @@ export interface FileEntry { filePath?: string | null; contentType?: string | null; selected: boolean; + annotations?: Annotation[]; } export type FileList = FileEntry[]; diff --git a/packages/bruno-schema-types/src/common/index.ts b/packages/bruno-schema-types/src/common/index.ts index d19826f59..c50cd3eec 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 8007393f2..a3cf4d04d 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/multipart-form.ts b/packages/bruno-schema-types/src/common/multipart-form.ts index 75200868d..f892d0089 100644 --- a/packages/bruno-schema-types/src/common/multipart-form.ts +++ b/packages/bruno-schema-types/src/common/multipart-form.ts @@ -1,3 +1,4 @@ +import { Annotation } from './annotation'; import type { UID } from './uid'; export interface MultipartFormEntry { @@ -8,6 +9,7 @@ export interface MultipartFormEntry { description?: string | null; contentType?: string | null; enabled?: boolean; + annotations?: Annotation[]; } export type MultipartForm = MultipartFormEntry[]; diff --git a/packages/bruno-schema-types/src/common/variables.ts b/packages/bruno-schema-types/src/common/variables.ts index 8d45d1401..a96f4510b 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 000000000..6c83c09f9 --- /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 b618af6f6..bbb549163 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 };