@usebruno/filestore 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/workers/worker-script.js +1 -1
  4. package/dist/cjs/workers/worker-script.js.map +1 -1
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/esm/index.js +1 -1
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/esm/workers/worker-script.js +1 -1
  10. package/dist/esm/workers/worker-script.js.map +1 -1
  11. package/dist/formats/bru/index.d.ts.map +1 -1
  12. package/dist/formats/bru/utils/redact-large-text-blocks.d.ts +11 -0
  13. package/dist/formats/bru/utils/redact-large-text-blocks.d.ts.map +1 -0
  14. package/dist/formats/yml/common/app.d.ts +8 -0
  15. package/dist/formats/yml/common/app.d.ts.map +1 -0
  16. package/dist/formats/yml/common/auth.d.ts.map +1 -1
  17. package/dist/formats/yml/common/body.d.ts.map +1 -1
  18. package/dist/formats/yml/common/datatype.d.ts +14 -0
  19. package/dist/formats/yml/common/datatype.d.ts.map +1 -0
  20. package/dist/formats/yml/common/headers.d.ts.map +1 -1
  21. package/dist/formats/yml/common/variables.d.ts.map +1 -1
  22. package/dist/formats/yml/items/parseApp.d.ts +13 -0
  23. package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
  24. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  25. package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
  26. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  27. package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
  28. package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
  29. package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
  30. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  31. package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
  32. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  33. package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
  34. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  35. package/dist/formats/yml/parseEnvironment.d.ts +3 -1
  36. package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
  37. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  38. package/dist/formats/yml/parseItem.d.ts.map +1 -1
  39. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  40. package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
  41. package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
  42. package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/types.d.ts +6 -0
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
  48. package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
  49. package/package.json +7 -5
  50. package/src/constants.ts +9 -0
  51. package/src/formats/bru/index.ts +52 -0
  52. package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
  53. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +313 -0
  54. package/src/formats/bru/utils/redact-large-text-blocks.ts +102 -0
  55. package/src/formats/yml/common/actions.ts +1 -1
  56. package/src/formats/yml/common/app.ts +27 -0
  57. package/src/formats/yml/common/auth.ts +53 -1
  58. package/src/formats/yml/common/body.spec.ts +62 -0
  59. package/src/formats/yml/common/body.ts +27 -9
  60. package/src/formats/yml/common/datatype.spec.ts +110 -0
  61. package/src/formats/yml/common/datatype.ts +57 -0
  62. package/src/formats/yml/common/headers.ts +4 -0
  63. package/src/formats/yml/common/variables.spec.ts +127 -0
  64. package/src/formats/yml/common/variables.ts +20 -6
  65. package/src/formats/yml/items/parseApp.ts +37 -0
  66. package/src/formats/yml/items/parseGraphQLRequest.ts +8 -0
  67. package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
  68. package/src/formats/yml/items/parseHttpRequest.ts +13 -0
  69. package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
  70. package/src/formats/yml/items/stringifyApp.ts +45 -0
  71. package/src/formats/yml/items/stringifyGraphQLRequest.ts +3 -0
  72. package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
  73. package/src/formats/yml/items/stringifyHttpRequest.ts +10 -0
  74. package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
  75. package/src/formats/yml/parseCollection.spec.ts +70 -0
  76. package/src/formats/yml/parseCollection.ts +8 -3
  77. package/src/formats/yml/parseEnvironment.spec.ts +99 -0
  78. package/src/formats/yml/parseEnvironment.ts +69 -5
  79. package/src/formats/yml/parseFolder.spec.ts +52 -0
  80. package/src/formats/yml/parseFolder.ts +14 -21
  81. package/src/formats/yml/parseItem.spec.ts +147 -0
  82. package/src/formats/yml/parseItem.ts +4 -0
  83. package/src/formats/yml/stringifyCollection.spec.ts +77 -0
  84. package/src/formats/yml/stringifyCollection.ts +4 -0
  85. package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
  86. package/src/formats/yml/stringifyEnvironment.ts +59 -10
  87. package/src/formats/yml/stringifyFolder.spec.ts +47 -0
  88. package/src/formats/yml/stringifyItem.spec.ts +53 -0
  89. package/src/formats/yml/stringifyItem.ts +4 -0
  90. package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
  91. package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
  92. package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
  93. package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
  94. package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
  95. package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
  96. package/src/formats/yml/tests/parseCollection.spec.js +63 -0
  97. package/src/formats/yml/tests/parseEnvironment.spec.js +122 -0
  98. package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
  99. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +204 -0
  100. package/src/formats/yml/tests/variables.spec.js +34 -0
  101. package/src/index.ts +5 -0
  102. package/src/types/bruno-lang.d.ts +1 -0
  103. package/src/types.ts +7 -0
  104. package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
  105. package/src/utils/secretsJsonToEnvironments.ts +129 -0
  106. package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
  107. package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
  108. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
  109. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
  110. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
  111. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
  112. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
  113. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
@@ -0,0 +1,110 @@
1
+ import {
2
+ isBrunoVariableDataType,
3
+ isTypedValue,
4
+ hasTypedMetadata,
5
+ toOpenCollectionTypedValue,
6
+ fromOpenCollectionTypedValue
7
+ } from './datatype';
8
+
9
+ describe('dataType helpers', () => {
10
+ describe('isBrunoVariableDataType', () => {
11
+ it('returns true for supported Bruno datatypes', () => {
12
+ expect(isBrunoVariableDataType('string')).toBe(true);
13
+ expect(isBrunoVariableDataType('number')).toBe(true);
14
+ expect(isBrunoVariableDataType('boolean')).toBe(true);
15
+ expect(isBrunoVariableDataType('object')).toBe(true);
16
+ });
17
+
18
+ it('returns false for unsupported OpenCollection datatypes (e.g. null)', () => {
19
+ expect(isBrunoVariableDataType('null')).toBe(false);
20
+ });
21
+
22
+ it('returns false for non-string inputs', () => {
23
+ expect(isBrunoVariableDataType(undefined)).toBe(false);
24
+ expect(isBrunoVariableDataType(null)).toBe(false);
25
+ expect(isBrunoVariableDataType(42)).toBe(false);
26
+ expect(isBrunoVariableDataType({})).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('isTypedValue', () => {
31
+ it('returns true for OpenCollection typed-value shapes', () => {
32
+ expect(isTypedValue({ type: 'number', data: '42' })).toBe(true);
33
+ expect(isTypedValue({ type: 'object', data: '{}' })).toBe(true);
34
+ });
35
+
36
+ it('returns false for plain strings', () => {
37
+ expect(isTypedValue('hello')).toBe(false);
38
+ expect(isTypedValue('')).toBe(false);
39
+ });
40
+
41
+ it('returns false for arrays, null, and objects missing type/data', () => {
42
+ expect(isTypedValue(null)).toBe(false);
43
+ expect(isTypedValue([])).toBe(false);
44
+ expect(isTypedValue([{ type: 'number', data: '1' }])).toBe(false);
45
+ expect(isTypedValue({ type: 'number' })).toBe(false);
46
+ expect(isTypedValue({ data: '1' })).toBe(false);
47
+ expect(isTypedValue({})).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('hasTypedMetadata', () => {
52
+ it('returns false when dataType is missing', () => {
53
+ expect(hasTypedMetadata({})).toBe(false);
54
+ });
55
+
56
+ it('returns false when dataType is the string default', () => {
57
+ expect(hasTypedMetadata({ dataType: 'string' })).toBe(false);
58
+ });
59
+
60
+ it('returns true for non-string datatypes', () => {
61
+ expect(hasTypedMetadata({ dataType: 'number' })).toBe(true);
62
+ expect(hasTypedMetadata({ dataType: 'boolean' })).toBe(true);
63
+ expect(hasTypedMetadata({ dataType: 'object' })).toBe(true);
64
+ });
65
+ });
66
+
67
+ describe('toOpenCollectionTypedValue', () => {
68
+ it('builds a {type, data} struct from dataType + stringified value', () => {
69
+ expect(toOpenCollectionTypedValue({ dataType: 'number' }, '42'))
70
+ .toEqual({ type: 'number', data: '42' });
71
+ expect(toOpenCollectionTypedValue({ dataType: 'boolean' }, 'true'))
72
+ .toEqual({ type: 'boolean', data: 'true' });
73
+ expect(toOpenCollectionTypedValue({ dataType: 'object' }, '{"a":1}'))
74
+ .toEqual({ type: 'object', data: '{"a":1}' });
75
+ });
76
+
77
+ it('falls back to type "string" when dataType is missing', () => {
78
+ expect(toOpenCollectionTypedValue({}, 'hello'))
79
+ .toEqual({ type: 'string', data: 'hello' });
80
+ });
81
+ });
82
+
83
+ describe('fromOpenCollectionTypedValue', () => {
84
+ it('coerces typed values to their declared dataType', () => {
85
+ expect(fromOpenCollectionTypedValue({ type: 'number', data: '42' }))
86
+ .toEqual({ value: 42, dataType: 'number' });
87
+ expect(fromOpenCollectionTypedValue({ type: 'boolean', data: 'true' }))
88
+ .toEqual({ value: true, dataType: 'boolean' });
89
+ expect(fromOpenCollectionTypedValue({ type: 'object', data: '{"a":1}' }))
90
+ .toEqual({ value: { a: 1 }, dataType: 'object' });
91
+ });
92
+
93
+ it('falls back to string when the declared type is unsupported (e.g. null)', () => {
94
+ const result = fromOpenCollectionTypedValue({ type: 'null' as any, data: '' });
95
+ expect(result.dataType).toBeUndefined();
96
+ });
97
+
98
+ it('coerces non-string data to a string before coercing by dataType', () => {
99
+ const result = fromOpenCollectionTypedValue({ type: 'number', data: 42 as any });
100
+ expect(result.value).toBe(42);
101
+ expect(result.dataType).toBe('number');
102
+ });
103
+
104
+ it('returns the raw string when coercion fails (e.g. non-numeric @number)', () => {
105
+ const result = fromOpenCollectionTypedValue({ type: 'number', data: 'not-a-number' });
106
+ expect(result.value).toBe('not-a-number');
107
+ expect(result.dataType).toBe('number');
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,57 @@
1
+ import type { VariableTypedValue } from '@opencollection/types/common/variables';
2
+ import {
3
+ parseValueByDataType,
4
+ BrunoVariableDataType,
5
+ isBrunoVariableDataType,
6
+ valueToString
7
+ } from '@usebruno/common/utils';
8
+
9
+ export { BrunoVariableDataType, isBrunoVariableDataType };
10
+
11
+ export const serializeVariableValue = (value: unknown): string => {
12
+ if (value !== null && typeof value === 'object') {
13
+ return JSON.stringify(value, null, 2);
14
+ }
15
+ return valueToString(value);
16
+ };
17
+
18
+ export const isTypedValue = (value: unknown): value is VariableTypedValue => {
19
+ return typeof value === 'object'
20
+ && value !== null
21
+ && !Array.isArray(value)
22
+ && 'type' in value
23
+ && 'data' in value;
24
+ };
25
+
26
+ export interface TypedVariableFields {
27
+ dataType?: BrunoVariableDataType;
28
+ }
29
+
30
+ export const hasTypedMetadata = (v: TypedVariableFields): boolean => {
31
+ return !!v.dataType && v.dataType !== 'string';
32
+ };
33
+
34
+ export const toOpenCollectionTypedValue = (
35
+ v: TypedVariableFields,
36
+ dataStr: string
37
+ ): VariableTypedValue => {
38
+ return {
39
+ type: (v.dataType || 'string') as VariableTypedValue['type'],
40
+ data: dataStr
41
+ };
42
+ };
43
+
44
+ // `'string'` is the implicit default — omitted from the result.
45
+ export const fromOpenCollectionTypedValue = (
46
+ typed: VariableTypedValue
47
+ ): { value: any } & TypedVariableFields => {
48
+ const dataStr = typeof typed.data === 'string' ? typed.data : String(typed.data ?? '');
49
+ const dataType: BrunoVariableDataType = isBrunoVariableDataType(typed.type) ? typed.type : 'string';
50
+ const result: { value: any } & TypedVariableFields = {
51
+ value: parseValueByDataType(dataStr, dataType)
52
+ };
53
+ if (dataType !== 'string') {
54
+ result.dataType = dataType;
55
+ }
56
+ return result;
57
+ };
@@ -51,6 +51,10 @@ export const toBrunoHttpHeaders = (headers: HttpRequestHeader[] | HttpResponseHe
51
51
  enabled: ('disabled' in header) ? header.disabled !== true : true
52
52
  };
53
53
 
54
+ if ('description' in header && header.description) {
55
+ brunoHeader.description = typeof header.description === 'string' ? header.description : (header.description as any)?.content || '';
56
+ }
57
+
54
58
  return brunoHeader;
55
59
  });
56
60
 
@@ -0,0 +1,127 @@
1
+ import { toOpenCollectionVariables, toBrunoVariables } from './variables';
2
+
3
+ describe('toOpenCollectionVariables', () => {
4
+ it('returns undefined for null / empty input', () => {
5
+ expect(toOpenCollectionVariables(null)).toBeUndefined();
6
+ expect(toOpenCollectionVariables(undefined)).toBeUndefined();
7
+ expect(toOpenCollectionVariables([])).toBeUndefined();
8
+ expect(toOpenCollectionVariables({ req: [], res: [] })).toBeUndefined();
9
+ });
10
+
11
+ it('serializes plain string variables as a raw string value', () => {
12
+ const out = toOpenCollectionVariables([
13
+ { uid: 'u1', name: 'apiKey', value: 'abc', enabled: true } as any
14
+ ]);
15
+
16
+ expect(out).toEqual([{ name: 'apiKey', value: 'abc' }]);
17
+ });
18
+
19
+ it('serializes typed variables as a {type, data} struct', () => {
20
+ const out = toOpenCollectionVariables([
21
+ { uid: 'u1', name: 'port', value: 3000, enabled: true, dataType: 'number' } as any,
22
+ { uid: 'u2', name: 'flag', value: true, enabled: true, dataType: 'boolean' } as any,
23
+ { uid: 'u3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' } as any
24
+ ]);
25
+
26
+ expect(out).toEqual([
27
+ { name: 'port', value: { type: 'number', data: '3000' } },
28
+ { name: 'flag', value: { type: 'boolean', data: 'true' } },
29
+ { name: 'config', value: { type: 'object', data: '{\n "a": 1\n}' } }
30
+ ]);
31
+ });
32
+
33
+ it('does not emit a typed struct for the string default dataType', () => {
34
+ const out = toOpenCollectionVariables([
35
+ { uid: 'u1', name: 'apiKey', value: 'abc', enabled: true, dataType: 'string' } as any
36
+ ]);
37
+
38
+ expect(out).toEqual([{ name: 'apiKey', value: 'abc' }]);
39
+ });
40
+
41
+ it('marks disabled variables and preserves description', () => {
42
+ const out = toOpenCollectionVariables([
43
+ { uid: 'u1', name: 'apiKey', value: 'abc', enabled: false, description: 'auth key' } as any
44
+ ]);
45
+
46
+ expect(out).toEqual([
47
+ { name: 'apiKey', value: 'abc', description: 'auth key', disabled: true }
48
+ ]);
49
+ });
50
+
51
+ it('accepts the folder { req, res } shape and only emits req vars', () => {
52
+ const out = toOpenCollectionVariables({
53
+ req: [{ uid: 'u1', name: 'a', value: '1', enabled: true } as any],
54
+ res: [{ uid: 'u2', name: 'b', value: '2', enabled: true } as any]
55
+ });
56
+
57
+ expect(out).toEqual([{ name: 'a', value: '1' }]);
58
+ });
59
+ });
60
+
61
+ describe('toBrunoVariables', () => {
62
+ it('returns empty req/res for null / empty input', () => {
63
+ expect(toBrunoVariables(null)).toEqual({ req: [], res: [] });
64
+ expect(toBrunoVariables(undefined)).toEqual({ req: [], res: [] });
65
+ expect(toBrunoVariables([])).toEqual({ req: [], res: [] });
66
+ });
67
+
68
+ it('parses plain string variables into Bruno req vars', () => {
69
+ const { req } = toBrunoVariables([
70
+ { name: 'apiKey', value: 'abc' } as any
71
+ ]);
72
+
73
+ expect(req).toHaveLength(1);
74
+ const v = req![0];
75
+ expect(v).toMatchObject({
76
+ name: 'apiKey',
77
+ value: 'abc',
78
+ enabled: true,
79
+ local: false
80
+ });
81
+ expect(v.uid).toEqual(expect.any(String));
82
+ expect(v.dataType).toBeUndefined();
83
+ });
84
+
85
+ it('parses typed-value variables into typed Bruno variables', () => {
86
+ const { req } = toBrunoVariables([
87
+ { name: 'port', value: { type: 'number', data: '3000' } } as any,
88
+ { name: 'flag', value: { type: 'boolean', data: 'true' } } as any,
89
+ { name: 'config', value: { type: 'object', data: '{"a":1}' } } as any
90
+ ]);
91
+
92
+ expect(req![0]).toMatchObject({ name: 'port', value: 3000, dataType: 'number' });
93
+ expect(req![1]).toMatchObject({ name: 'flag', value: true, dataType: 'boolean' });
94
+ expect(req![2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
95
+ });
96
+
97
+ it('falls back to the raw string when typed data fails to coerce', () => {
98
+ const { req } = toBrunoVariables([
99
+ { name: 'port', value: { type: 'number', data: 'not-a-number' } } as any,
100
+ { name: 'flag', value: { type: 'boolean', data: 'not-a-boolean' } } as any,
101
+ { name: 'config', value: { type: 'object', data: 'not-a-object' } } as any
102
+ ]);
103
+
104
+ expect(req![0]).toMatchObject({ name: 'port', value: 'not-a-number', dataType: 'number' });
105
+ expect(typeof req![0].value).toBe('string');
106
+ expect(req![1]).toMatchObject({ name: 'flag', value: 'not-a-boolean', dataType: 'boolean' });
107
+ expect(typeof req![1].value).toBe('string');
108
+ expect(req![2]).toMatchObject({ name: 'config', value: 'not-a-object', dataType: 'object' });
109
+ expect(typeof req![2].value).toBe('string');
110
+ });
111
+
112
+ it('respects the disabled flag', () => {
113
+ const { req } = toBrunoVariables([
114
+ { name: 'a', value: '1', disabled: true } as any
115
+ ]);
116
+
117
+ expect(req![0].enabled).toBe(false);
118
+ });
119
+
120
+ it('extracts description.content when description is a structured object', () => {
121
+ const { req } = toBrunoVariables([
122
+ { name: 'a', value: '1', description: { content: 'note' } } as any
123
+ ]);
124
+
125
+ expect(req![0].description).toBe('note');
126
+ });
127
+ });
@@ -1,7 +1,14 @@
1
- import { Variable } from '@opencollection/types/common/variables';
1
+ import { Variable, VariableTypedValue } from '@opencollection/types/common/variables';
2
2
  import { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';
3
3
  import { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';
4
4
  import { uuid, ensureString } from '../../../utils';
5
+ import {
6
+ isTypedValue,
7
+ hasTypedMetadata,
8
+ toOpenCollectionTypedValue,
9
+ fromOpenCollectionTypedValue,
10
+ serializeVariableValue
11
+ } from './datatype';
5
12
 
6
13
  /**
7
14
  * Convert Bruno pre-request variables to OpenCollection variables format.
@@ -19,9 +26,10 @@ export const toOpenCollectionVariables = (variables: BrunoFolderRequest['vars']
19
26
  }
20
27
 
21
28
  const ocVariables: Variable[] = reqVarsArray.map((v: BrunoVariable): Variable => {
29
+ const valueStr = serializeVariableValue(v.value);
22
30
  const variable: Variable = {
23
31
  name: v.name || '',
24
- value: v.value || ''
32
+ value: hasTypedMetadata(v) ? toOpenCollectionTypedValue(v, valueStr) : valueStr
25
33
  };
26
34
 
27
35
  if (v?.description?.trim().length) {
@@ -49,19 +57,25 @@ export const toBrunoVariables = (variables: Variable[] | null | undefined): { re
49
57
  const reqVars: BrunoVariables = [];
50
58
 
51
59
  variables.forEach((v: Variable) => {
52
- const variable: BrunoVariable = {
60
+ const base: BrunoVariable = {
53
61
  uid: uuid(),
54
62
  name: ensureString(v.name),
55
- value: ensureString(v.value),
63
+ value: '',
56
64
  enabled: v.disabled !== true,
57
65
  local: false
58
66
  };
59
67
 
68
+ if (isTypedValue(v.value)) {
69
+ Object.assign(base, fromOpenCollectionTypedValue(v.value));
70
+ } else {
71
+ base.value = ensureString(v.value);
72
+ }
73
+
60
74
  if (v.description) {
61
- variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
75
+ base.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
62
76
  }
63
77
 
64
- reqVars.push(variable);
78
+ reqVars.push(base);
65
79
  });
66
80
 
67
81
  return { req: reqVars, res: [] };
@@ -0,0 +1,37 @@
1
+ import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
2
+ import { uuid } from '../../../utils';
3
+
4
+ export interface AppFile {
5
+ info: {
6
+ name?: string;
7
+ type: 'app';
8
+ seq?: number;
9
+ tags?: string[];
10
+ };
11
+ code?: string;
12
+ }
13
+
14
+ const parseApp = (ocApp: AppFile): BrunoItem => {
15
+ const info = ocApp.info || ({} as AppFile['info']);
16
+
17
+ const brunoItem: BrunoItem = {
18
+ uid: uuid(),
19
+ type: 'app',
20
+ seq: typeof info.seq === 'number' ? info.seq : 1,
21
+ name: info.name || 'App',
22
+ tags: Array.isArray(info.tags) ? info.tags : [],
23
+ request: null,
24
+ settings: null,
25
+ app: { code: ocApp.code || '' },
26
+ fileContent: null,
27
+ root: null,
28
+ items: [],
29
+ examples: [],
30
+ filename: null,
31
+ pathname: null
32
+ };
33
+
34
+ return brunoItem;
35
+ };
36
+
37
+ export default parseApp;
@@ -98,6 +98,14 @@ const parseGraphQLRequest = (ocRequest: GraphQLRequest): BrunoItem => {
98
98
  pathname: null
99
99
  };
100
100
 
101
+ // description
102
+ if (info?.description) {
103
+ const desc = typeof info.description === 'string' ? info.description : (info.description as any)?.content || '';
104
+ if (desc.trim().length) {
105
+ brunoItem.description = desc;
106
+ }
107
+ }
108
+
101
109
  // settings
102
110
  if (ocRequest.settings) {
103
111
  const settings: BrunoHttpItemSettings = {};
@@ -1,6 +1,6 @@
1
1
  import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
2
2
  import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
3
- import type { GrpcRequest, GrpcMetadata } from '@opencollection/types/requests/grpc';
3
+ import type { GrpcRequest, GrpcMetadata, GrpcMessageVariant } from '@opencollection/types/requests/grpc';
4
4
  import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
5
5
  import { toBrunoAuth } from '../common/auth';
6
6
  import { toBrunoVariables } from '../common/variables';
@@ -21,6 +21,13 @@ const toBrunoGrpcMetadata = (metadata: GrpcMetadata[] | null | undefined): Bruno
21
21
  enabled: meta.disabled !== true
22
22
  };
23
23
 
24
+ if (meta.description) {
25
+ const desc = typeof meta.description === 'string' ? meta.description : (meta.description as any)?.content || '';
26
+ if (desc.trim().length) {
27
+ brunoMeta.description = desc;
28
+ }
29
+ }
30
+
24
31
  return brunoMeta;
25
32
  });
26
33
 
@@ -57,10 +64,16 @@ const parseGrpcRequest = (ocRequest: GrpcRequest): BrunoItem => {
57
64
  };
58
65
 
59
66
  // message
60
- if (isNonEmptyString(grpc?.message)) {
67
+ const rawMessage = grpc?.message;
68
+ if (Array.isArray(rawMessage)) {
69
+ brunoRequest.body.grpc = (rawMessage as GrpcMessageVariant[]).map(({ title, message }, index) => ({
70
+ name: title || `message ${index + 1}`,
71
+ content: ensureString(message)
72
+ }));
73
+ } else if (isNonEmptyString(rawMessage)) {
61
74
  brunoRequest.body.grpc = [{
62
75
  name: '',
63
- content: grpc?.message as string
76
+ content: rawMessage as string
64
77
  }];
65
78
  }
66
79
 
@@ -110,6 +123,14 @@ const parseGrpcRequest = (ocRequest: GrpcRequest): BrunoItem => {
110
123
  pathname: null
111
124
  };
112
125
 
126
+ // description
127
+ if (info?.description) {
128
+ const desc = typeof info.description === 'string' ? info.description : (info.description as any)?.content || '';
129
+ if (desc.trim().length) {
130
+ brunoItem.description = desc;
131
+ }
132
+ }
133
+
113
134
  return brunoItem;
114
135
  };
115
136
 
@@ -9,6 +9,7 @@ import { toBrunoVariables } from '../common/variables';
9
9
  import { toBrunoPostResponseVariables } from '../common/actions';
10
10
  import { toBrunoScripts } from '../common/scripts';
11
11
  import { toBrunoAssertions } from '../common/assertions';
12
+ import { toBrunoApp } from '../common/app';
12
13
  import { uuid, ensureString } from '../../../utils';
13
14
 
14
15
  const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
@@ -79,6 +80,9 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
79
80
  brunoRequest.docs = ocRequest.docs;
80
81
  }
81
82
 
83
+ // app
84
+ const app = toBrunoApp((ocRequest as any).app);
85
+
82
86
  // bruno item
83
87
  const brunoItem: BrunoItem = {
84
88
  uid: uuid(),
@@ -88,6 +92,7 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
88
92
  tags: info?.tags || [],
89
93
  request: brunoRequest,
90
94
  settings: null,
95
+ app,
91
96
  fileContent: null,
92
97
  root: null,
93
98
  items: [],
@@ -96,6 +101,14 @@ const parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {
96
101
  pathname: null
97
102
  };
98
103
 
104
+ // description
105
+ if (info?.description) {
106
+ const desc = typeof info.description === 'string' ? info.description : (info.description as any)?.content || '';
107
+ if (desc.trim().length) {
108
+ brunoItem.description = desc;
109
+ }
110
+ }
111
+
99
112
  // settings
100
113
  if (ocRequest.settings) {
101
114
  const settings: BrunoHttpItemSettings = {};
@@ -1,6 +1,6 @@
1
1
  import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
2
2
  import type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';
3
- import type { WebSocketRequest, WebSocketMessage } from '@opencollection/types/requests/websocket';
3
+ import type { WebSocketRequest, WebSocketMessage, WebSocketMessageVariant } from '@opencollection/types/requests/websocket';
4
4
  import { toBrunoAuth } from '../common/auth';
5
5
  import { toBrunoHttpHeaders } from '../common/headers';
6
6
  import { toBrunoVariables } from '../common/variables';
@@ -35,14 +35,26 @@ const parseWebsocketRequest = (ocRequest: WebSocketRequest): BrunoItem => {
35
35
 
36
36
  // message
37
37
  if (websocket?.message) {
38
- const message = websocket.message as WebSocketMessage;
39
- const messageData = ensureString(message.data);
40
- if (messageData.trim().length) {
41
- brunoRequest.body.ws = [{
42
- name: '',
43
- type: message.type || 'text',
44
- content: messageData
45
- }];
38
+ if (Array.isArray(websocket.message)) {
39
+ // multiple messages: WebSocketMessageVariant[]
40
+ const variants = websocket.message as WebSocketMessageVariant[];
41
+ brunoRequest.body.ws = variants.map((variant, index) => ({
42
+ name: variant.title || `message ${index + 1}`,
43
+ type: variant.message?.type || 'text',
44
+ content: ensureString(variant.message?.data),
45
+ selected: variant.selected || false
46
+ }));
47
+ } else {
48
+ // single message uses flat WebSocketMessage
49
+ const message = websocket.message as WebSocketMessage;
50
+ const messageData = ensureString(message.data);
51
+ if (messageData.trim().length) {
52
+ brunoRequest.body.ws = [{
53
+ name: '',
54
+ type: message.type || 'text',
55
+ content: messageData
56
+ }];
57
+ }
46
58
  }
47
59
  }
48
60
 
@@ -101,6 +113,14 @@ const parseWebsocketRequest = (ocRequest: WebSocketRequest): BrunoItem => {
101
113
  pathname: null
102
114
  };
103
115
 
116
+ // description
117
+ if (info?.description) {
118
+ const desc = typeof info.description === 'string' ? info.description : (info.description as any)?.content || '';
119
+ if (desc.trim().length) {
120
+ brunoItem.description = desc;
121
+ }
122
+ }
123
+
104
124
  return brunoItem;
105
125
  };
106
126
 
@@ -0,0 +1,45 @@
1
+ import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
2
+ import { stringifyYml } from '../utils';
3
+
4
+ interface AppFileInfo {
5
+ name: string;
6
+ type: 'app';
7
+ seq?: number;
8
+ tags?: string[];
9
+ }
10
+
11
+ interface AppFile {
12
+ info: AppFileInfo;
13
+ code?: string;
14
+ }
15
+
16
+ // Mirror the request shape (info.name/type/seq/tags + body) so apps round-trip
17
+ // the same fields and can be reordered via seq like every other item.
18
+ const stringifyApp = (item: BrunoItem): string => {
19
+ try {
20
+ const info: AppFileInfo = {
21
+ name: item.name && item.name.trim().length ? item.name : 'App',
22
+ type: 'app'
23
+ };
24
+ if (typeof item.seq === 'number') {
25
+ info.seq = item.seq;
26
+ }
27
+ if (Array.isArray(item.tags) && item.tags.length) {
28
+ info.tags = item.tags;
29
+ }
30
+
31
+ const ocApp: AppFile = { info };
32
+
33
+ const code = item.app?.code;
34
+ if (code && code.trim().length) {
35
+ ocApp.code = code;
36
+ }
37
+
38
+ return stringifyYml(ocApp);
39
+ } catch (error) {
40
+ console.error('Error stringifying app:', error);
41
+ throw error;
42
+ }
43
+ };
44
+
45
+ export default stringifyApp;
@@ -33,6 +33,9 @@ const stringifyGraphQLRequest = (item: BrunoItem): string => {
33
33
  if (item.tags?.length) {
34
34
  info.tags = item.tags;
35
35
  }
36
+ if (isNonEmptyString(item.description)) {
37
+ info.description = item.description;
38
+ }
36
39
  ocRequest.info = info;
37
40
 
38
41
  // graphql block
@@ -1,7 +1,7 @@
1
1
  import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
2
2
  import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
3
3
  import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
4
- import type { GrpcRequest, GrpcMetadata, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';
4
+ import type { GrpcRequest, GrpcMetadata, GrpcMessageVariant, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';
5
5
  import type { Auth } from '@opencollection/types/common/auth';
6
6
  import type { Scripts } from '@opencollection/types/common/scripts';
7
7
  import type { Variable } from '@opencollection/types/common/variables';
@@ -29,6 +29,9 @@ const stringifyGrpcRequest = (item: BrunoItem): string => {
29
29
  if (item.tags?.length) {
30
30
  info.tags = item.tags;
31
31
  }
32
+ if (isNonEmptyString(item.description)) {
33
+ info.description = item.description;
34
+ }
32
35
  ocRequest.info = info;
33
36
 
34
37
  // grpc block
@@ -73,16 +76,11 @@ const stringifyGrpcRequest = (item: BrunoItem): string => {
73
76
 
74
77
  // message
75
78
  if (brunoRequest.body?.mode === 'grpc' && brunoRequest.body.grpc?.length) {
76
- const messages = brunoRequest.body.grpc;
77
-
78
- // todo: bruno app supports only one message for now
79
- // update this when bruno app supports multiple messages
80
- if (messages.length) {
81
- const message: GrpcMessage = messages[0].content || '';
82
- if (message.trim().length) {
83
- grpc.message = message;
84
- }
85
- }
79
+ const messages: GrpcMessageVariant[] = brunoRequest.body.grpc.map(({ name, content }, index) => ({
80
+ title: name || `message ${index + 1}`,
81
+ message: content || ''
82
+ }));
83
+ grpc.message = messages;
86
84
  }
87
85
 
88
86
  // auth