mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: avoid round trip loss of annotation data (#7730)
* fix: avoid round trip loss of annotation data * feat: update types for file , multipart and tests for the same * chore: optional * chore: fix body:file annotation * chore: remove log
This commit is contained in:
@@ -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
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
)}`;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
7
packages/bruno-schema-types/src/common/annotation.ts
Normal file
7
packages/bruno-schema-types/src/common/annotation.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Annotation applied to pairs (headers, vars, params, etc.)
|
||||
*/
|
||||
export interface Annotation {
|
||||
name: string;
|
||||
value?: string | null;
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user