@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
@@ -8,6 +8,7 @@ import type { Assertion } from '@opencollection/types/common/assertions';
8
8
  import type { Action } from '@opencollection/types/common/actions';
9
9
  import type { HttpRequestParam, HttpRequestBody } from '@opencollection/types/requests/http';
10
10
  import { stringifyYml } from '../utils';
11
+ import { toOpenCollectionApp, OpenCollectionApp } from '../common/app';
11
12
  import { toOpenCollectionAuth } from '../common/auth';
12
13
  import { toOpenCollectionHttpHeaders, toOpenCollectionResponseHeaders } from '../common/headers';
13
14
  import { toOpenCollectionParams } from '../common/params';
@@ -34,6 +35,9 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
34
35
  if (item.tags?.length) {
35
36
  info.tags = item.tags;
36
37
  }
38
+ if (isNonEmptyString(item.description)) {
39
+ info.description = item.description;
40
+ }
37
41
  ocRequest.info = info;
38
42
 
39
43
  // http block
@@ -212,6 +216,12 @@ const stringifyHttpRequest = (item: BrunoItem): string => {
212
216
  ocRequest.docs = brunoRequest.docs;
213
217
  }
214
218
 
219
+ // app
220
+ const app: OpenCollectionApp | undefined = toOpenCollectionApp(item.app);
221
+ if (app) {
222
+ (ocRequest as any).app = app;
223
+ }
224
+
215
225
  return stringifyYml(ocRequest);
216
226
  } catch (error) {
217
227
  console.error('Error stringifying HTTP request:', error);
@@ -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, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime } from '@opencollection/types/requests/websocket';
3
+ import type { WebSocketRequest, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime, WebSocketMessage, WebSocketMessageVariant } from '@opencollection/types/requests/websocket';
4
4
  import type { Auth } from '@opencollection/types/common/auth';
5
5
  import type { Scripts } from '@opencollection/types/common/scripts';
6
6
  import type { Variable } from '@opencollection/types/common/variables';
@@ -28,6 +28,9 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => {
28
28
  if (item.tags?.length) {
29
29
  info.tags = item.tags;
30
30
  }
31
+ if (isNonEmptyString(item.description)) {
32
+ info.description = item.description;
33
+ }
31
34
  ocRequest.info = info;
32
35
 
33
36
  // websocket block
@@ -41,21 +44,31 @@ const stringifyWebsocketRequest = (item: BrunoItem): string => {
41
44
  websocket.headers = headers;
42
45
  }
43
46
 
44
- // message
47
+ // message: single message without a custom name uses flat WebSocketMessage (backward compatible),
48
+ // otherwise uses WebSocketMessageVariant[] to preserve names
45
49
  if (brunoRequest.body?.mode === 'ws' && brunoRequest.body.ws?.length) {
46
50
  const messages = brunoRequest.body.ws;
51
+ const hasCustomName = messages.length === 1 && messages[0].name && messages[0].name.trim().length > 0;
52
+
53
+ const hasContent = messages.length === 1 && (messages[0].content || '').trim().length > 0;
47
54
 
48
- // todo: bruno app supports only one message for now
49
- // update this when bruno app supports multiple messages
50
- if (messages.length) {
55
+ if (messages.length === 1 && !hasCustomName && hasContent) {
51
56
  const msg = messages[0];
52
57
  const message: WebSocketMessage = {
53
- type: (msg.type as 'text' | 'json' | 'xml' | 'binary') || 'text',
58
+ type: (msg.type as WebSocketMessage['type']) || 'text',
54
59
  data: msg.content || ''
55
60
  };
56
- if (message.data.trim().length) {
57
- websocket.message = message;
58
- }
61
+ websocket.message = message;
62
+ } else {
63
+ const variants: WebSocketMessageVariant[] = messages.map((msg, index) => ({
64
+ title: msg.name || `message ${index + 1}`,
65
+ selected: msg.selected || false,
66
+ message: {
67
+ type: (msg.type as WebSocketMessage['type']) || 'text',
68
+ data: msg.content || ''
69
+ }
70
+ }));
71
+ websocket.message = variants;
59
72
  }
60
73
  }
61
74
 
@@ -0,0 +1,70 @@
1
+ import parseCollection from './parseCollection';
2
+
3
+ // Typed `request.variables` propagate through parseCollection into the
4
+ // collectionRoot's vars.req with their dataType + coerced value.
5
+
6
+ describe('parseCollection — typed request.variables', () => {
7
+ it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
8
+ const yml = `opencollection: "1.0.0"
9
+ info:
10
+ name: c
11
+
12
+ request:
13
+ variables:
14
+ - name: count
15
+ value:
16
+ type: number
17
+ data: "42"
18
+ - name: enabled
19
+ value:
20
+ type: boolean
21
+ data: "true"
22
+ - name: config
23
+ value:
24
+ type: object
25
+ data: '{"a":1}'
26
+ - name: greeting
27
+ value:
28
+ type: string
29
+ data: hi
30
+ - name: plain
31
+ value: hello
32
+ - name: cfg
33
+ value:
34
+ type: object
35
+ data: 'not-json'
36
+ `;
37
+
38
+ const { collectionRoot } = parseCollection(yml);
39
+ const reqVars = collectionRoot.request!.vars!.req!;
40
+
41
+ expect(reqVars).toHaveLength(6);
42
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
43
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
44
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
45
+ // Explicit `type: string` is the implicit default — no dataType materialized.
46
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
47
+ expect(reqVars[3].dataType).toBeUndefined();
48
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
49
+ expect(reqVars[4].dataType).toBeUndefined();
50
+ // Un-coercible: raw value preserved for the UI mismatch warning.
51
+ expect(reqVars[5]).toMatchObject({ name: 'cfg', value: 'not-json', dataType: 'object' });
52
+ });
53
+ });
54
+
55
+ describe('parseCollection — reading the collection version', () => {
56
+ it('reads the version from the file as text', () => {
57
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: v1.0.0\n');
58
+ expect(brunoConfig.version).toBe('v1.0.0');
59
+ });
60
+
61
+ it('turns a number version into text (2 becomes "2")', () => {
62
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n version: 2\n');
63
+ expect(brunoConfig.version).toBe('2');
64
+ });
65
+
66
+ it('has no version when the file does not have one', () => {
67
+ const { brunoConfig } = parseCollection('opencollection: "1.0.0"\ninfo:\n name: c\n');
68
+ expect(brunoConfig.version).toBeUndefined();
69
+ });
70
+ });
@@ -7,6 +7,7 @@ import { toBrunoVariables } from './common/variables';
7
7
  import { toBrunoPostResponseVariables } from './common/actions';
8
8
  import { toBrunoScripts } from './common/scripts';
9
9
  import { ensureString } from '../../utils';
10
+ import type { BrunoPresetsExtension } from '../../types';
10
11
 
11
12
  interface ParsedCollection {
12
13
  collectionRoot: FolderRoot;
@@ -25,6 +26,10 @@ const parseCollection = (ymlString: string): ParsedCollection => {
25
26
  ignore: []
26
27
  };
27
28
 
29
+ if (oc.info?.version != null && oc.info.version !== '') {
30
+ brunoConfig.version = ensureString(oc.info.version, '');
31
+ }
32
+
28
33
  const brunoExtension = (oc.extensions as any)?.bruno;
29
34
  if (brunoExtension?.ignore && Array.isArray(brunoExtension.ignore)) {
30
35
  brunoConfig.ignore = brunoExtension.ignore;
@@ -32,11 +37,11 @@ const parseCollection = (ymlString: string): ParsedCollection => {
32
37
 
33
38
  // presets
34
39
  if (brunoExtension?.presets) {
35
- const presets = brunoExtension.presets as any;
40
+ const presets = brunoExtension.presets as BrunoPresetsExtension;
36
41
  if (presets.request) {
37
42
  brunoConfig.presets = {
38
- requestType: presets.request.type || [],
39
- requestUrl: presets.request.url || []
43
+ requestType: presets.request.type || '',
44
+ requestUrl: presets.request.url || ''
40
45
  };
41
46
  }
42
47
  }
@@ -0,0 +1,99 @@
1
+ import parseEnvironment, { toBrunoEnvironmentVariables } from './parseEnvironment';
2
+
3
+ describe('toBrunoEnvironmentVariables', () => {
4
+ it('returns an empty array for null / empty input', () => {
5
+ expect(toBrunoEnvironmentVariables(null)).toEqual([]);
6
+ expect(toBrunoEnvironmentVariables(undefined)).toEqual([]);
7
+ expect(toBrunoEnvironmentVariables([])).toEqual([]);
8
+ });
9
+
10
+ it('parses a plain string variable as text with secret=false', () => {
11
+ const [variable] = toBrunoEnvironmentVariables([{ name: 'apiKey', value: 'abc' } as any]);
12
+
13
+ expect(variable).toMatchObject({
14
+ name: 'apiKey',
15
+ value: 'abc',
16
+ type: 'text',
17
+ enabled: true,
18
+ secret: false
19
+ });
20
+ expect(variable.uid).toEqual(expect.any(String));
21
+ expect(variable.dataType).toBeUndefined();
22
+ });
23
+
24
+ it('parses typed values and attaches the dataType field', () => {
25
+ const vars = toBrunoEnvironmentVariables([
26
+ { name: 'port', value: { type: 'number', data: '300' } } as any,
27
+ { name: 'flag', value: { type: 'boolean', data: 'true' } } as any,
28
+ { name: 'config', value: { type: 'object', data: '{"a":1}' } } as any
29
+ ]);
30
+
31
+ expect(vars[0]).toMatchObject({ name: 'port', value: 300, dataType: 'number', secret: false });
32
+ expect(vars[1]).toMatchObject({ name: 'flag', value: true, dataType: 'boolean', secret: false });
33
+ expect(vars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object', secret: false });
34
+ });
35
+
36
+ it('parses secret variables with secret=true and attaches the dataType from `type`', () => {
37
+ const vars = toBrunoEnvironmentVariables([
38
+ { secret: true, name: 'apiKey' } as any,
39
+ { secret: true, name: 'port', type: 'number' } as any,
40
+ { secret: true, name: 'flag', type: 'boolean', disabled: true } as any
41
+ ]);
42
+
43
+ expect(vars[0]).toMatchObject({ name: 'apiKey', secret: true, enabled: true, value: '' });
44
+ // A bare secret has no `type`, so no dataType materializes.
45
+ expect(vars[0].dataType).toBeUndefined();
46
+ expect(vars[1]).toMatchObject({ name: 'port', secret: true, enabled: true, dataType: 'number' });
47
+ expect(vars[2]).toMatchObject({ name: 'flag', secret: true, enabled: false, dataType: 'boolean' });
48
+ });
49
+
50
+ it('honors the disabled flag on non-secret variables', () => {
51
+ const [variable] = toBrunoEnvironmentVariables([
52
+ { name: 'apiKey', value: 'abc', disabled: true } as any
53
+ ]);
54
+
55
+ expect(variable.enabled).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('parseEnvironment (full yml)', () => {
60
+ it('parses an environment with mixed plain, typed and secret variables', () => {
61
+ const yml = `name: test_env
62
+ variables:
63
+ - name: env_str
64
+ value: hello
65
+ - name: env_num
66
+ value:
67
+ type: number
68
+ data: "300"
69
+ - name: env_bool
70
+ value:
71
+ type: boolean
72
+ data: "true"
73
+ - name: env_obj
74
+ value:
75
+ type: object
76
+ data: '{"scope":"env"}'
77
+ - secret: true
78
+ name: env_secret
79
+ `;
80
+
81
+ const env = parseEnvironment(yml);
82
+
83
+ expect(env.name).toBe('test_env');
84
+ expect(env.variables).toHaveLength(5);
85
+
86
+ expect(env.variables[0]).toMatchObject({ name: 'env_str', value: 'hello', secret: false });
87
+ expect(env.variables[1]).toMatchObject({ name: 'env_num', value: 300, dataType: 'number', secret: false });
88
+ expect(env.variables[2]).toMatchObject({ name: 'env_bool', value: true, dataType: 'boolean', secret: false });
89
+ expect(env.variables[3]).toMatchObject({ name: 'env_obj', value: { scope: 'env' }, dataType: 'object', secret: false });
90
+ expect(env.variables[4]).toMatchObject({ name: 'env_secret', secret: true, value: '' });
91
+ expect(env.variables[4].dataType).toBeUndefined();
92
+ });
93
+
94
+ it('defaults environment name when missing', () => {
95
+ const env = parseEnvironment('variables: []');
96
+ expect(env.name).toBe('Untitled Environment');
97
+ expect(env.variables).toEqual([]);
98
+ });
99
+ });
@@ -1,21 +1,28 @@
1
- import type { Environment as BrunoEnvironment, EnvironmentVariable as BrunoEnvironmentVariable } from '@usebruno/schema-types/collection/environment';
1
+ import type {
2
+ Environment as BrunoEnvironment,
3
+ EnvironmentVariable as BrunoEnvironmentVariable,
4
+ SecretEnvironmentVariable as BrunoSecretEnvironmentVariable,
5
+ ExternalSecretProviderType as BrunoExternalSecretProviderType
6
+ } from '@usebruno/schema-types/collection/environment';
2
7
  import type { Environment } from '@opencollection/types/config/environments';
3
8
  import type { Variable, SecretVariable } from '@opencollection/types/common/variables';
4
9
  import { parseYml } from './utils';
5
10
  import { uuid, ensureString } from '../../utils';
11
+ import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
12
+ import { isTypedValue, fromOpenCollectionTypedValue } from './common/datatype';
6
13
 
7
14
  const isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {
8
15
  return 'secret' in v && v.secret === true;
9
16
  };
10
17
 
11
- const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): BrunoEnvironmentVariable[] => {
18
+ export const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[] => {
12
19
  if (!variables?.length) {
13
20
  return [];
14
21
  }
15
22
 
16
- return variables.map((v): BrunoEnvironmentVariable => {
23
+ return variables.map((v): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable) => {
17
24
  if (isSecretVariable(v)) {
18
- return {
25
+ const variable: BrunoEnvironmentVariable = {
19
26
  uid: uuid(),
20
27
  name: ensureString(v.name),
21
28
  value: '',
@@ -23,19 +30,71 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
23
30
  enabled: v.disabled !== true,
24
31
  secret: true
25
32
  };
33
+
34
+ if (v.type && v.type !== 'string' && v.type !== 'null') {
35
+ variable.dataType = v.type;
36
+ }
37
+
38
+ if (v.description) {
39
+ variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
40
+ }
41
+
42
+ return variable;
26
43
  }
44
+
27
45
  const variable: BrunoEnvironmentVariable = {
28
46
  uid: uuid(),
29
47
  name: ensureString(v.name),
30
- value: ensureString(v.value),
48
+ value: '',
31
49
  type: 'text',
32
50
  enabled: v.disabled !== true,
33
51
  secret: false
34
52
  };
53
+
54
+ if (isTypedValue(v.value)) {
55
+ Object.assign(variable, fromOpenCollectionTypedValue(v.value));
56
+ } else {
57
+ variable.value = ensureString(v.value);
58
+ }
59
+
60
+ if (v.description) {
61
+ variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
62
+ }
63
+
35
64
  return variable;
36
65
  });
37
66
  };
38
67
 
68
+ // Parses the yml `externalSecrets: { type, variables: [{ name, path|secretName|vaultName }] }`
69
+ // block into the runtime shape. `uid` and `enabled` are added here; the fetched `value`,
70
+ // `providerConfig` and `lastSyncedAt` are runtime-only and never present in the file.
71
+ const toBrunoExternalSecrets = (externalSecrets: any): BrunoEnvironment['externalSecrets'] | undefined => {
72
+ if (!externalSecrets || typeof externalSecrets !== 'object') {
73
+ return undefined;
74
+ }
75
+
76
+ const type = ensureString(externalSecrets.type) as BrunoExternalSecretProviderType;
77
+ const identifierField = SECRET_PROVIDER_IDENTIFIER_FIELDS[type];
78
+ if (!identifierField) {
79
+ return undefined;
80
+ }
81
+
82
+ // Keep any named entry, even one with an empty identifier — a half-filled secret row is the user's
83
+ // data; dropping it here would silently delete it on the next save.
84
+ const variables = Array.isArray(externalSecrets.variables)
85
+ ? externalSecrets.variables
86
+ .filter((v: any) => v && v.name)
87
+ .map((v: any) => ({
88
+ uid: uuid(),
89
+ name: ensureString(v.name),
90
+ enabled: v.disabled !== true,
91
+ [identifierField]: ensureString(v[identifierField])
92
+ }))
93
+ : [];
94
+
95
+ return { type, variables } as BrunoEnvironment['externalSecrets'];
96
+ };
97
+
39
98
  const parseEnvironment = (ymlString: string): BrunoEnvironment => {
40
99
  try {
41
100
  const ocEnvironment: Environment = parseYml(ymlString);
@@ -47,6 +106,11 @@ const parseEnvironment = (ymlString: string): BrunoEnvironment => {
47
106
  color: ocEnvironment.color || null
48
107
  };
49
108
 
109
+ const externalSecrets = toBrunoExternalSecrets((ocEnvironment as any).externalSecrets);
110
+ if (externalSecrets) {
111
+ brunoEnvironment.externalSecrets = externalSecrets;
112
+ }
113
+
50
114
  return brunoEnvironment;
51
115
  } catch (error) {
52
116
  console.error('Error parsing environment:', error);
@@ -0,0 +1,52 @@
1
+ import parseFolder from './parseFolder';
2
+
3
+ // Typed `request.variables` propagate through parseFolder into the folder's
4
+ // vars.req with their dataType + coerced value.
5
+
6
+ describe('parseFolder — typed request.variables', () => {
7
+ it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
8
+ const yml = `info:
9
+ name: my-folder
10
+
11
+ request:
12
+ variables:
13
+ - name: count
14
+ value:
15
+ type: number
16
+ data: "42"
17
+ - name: enabled
18
+ value:
19
+ type: boolean
20
+ data: "true"
21
+ - name: config
22
+ value:
23
+ type: object
24
+ data: '{"a":1}'
25
+ - name: greeting
26
+ value:
27
+ type: string
28
+ data: hi
29
+ - name: plain
30
+ value: hello
31
+ - name: flag
32
+ value:
33
+ type: boolean
34
+ data: 'maybe'
35
+ `;
36
+
37
+ const folder = parseFolder(yml);
38
+ const reqVars = folder.request!.vars!.req!;
39
+
40
+ expect(reqVars).toHaveLength(6);
41
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
42
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
43
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
44
+ // Explicit `type: string` is the implicit default — no dataType materialized.
45
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
46
+ expect(reqVars[3].dataType).toBeUndefined();
47
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
48
+ expect(reqVars[4].dataType).toBeUndefined();
49
+ // Un-coercible: raw value preserved for the UI mismatch warning.
50
+ expect(reqVars[5]).toMatchObject({ name: 'flag', value: 'maybe', dataType: 'boolean' });
51
+ });
52
+ });
@@ -19,15 +19,9 @@ const parseFolder = (ymlString: string): FolderRoot => {
19
19
  name: ensureString(info?.name, 'Untitled Folder'),
20
20
  seq: info?.seq || 1
21
21
  },
22
- request: null,
23
- docs: null
24
- };
25
-
26
- // request defaults
27
- if (ocFolder.request) {
28
- folderRoot.request = {
22
+ request: {
29
23
  headers: [],
30
- auth: null,
24
+ auth: toBrunoAuth(ocFolder.request?.auth),
31
25
  script: {
32
26
  req: null,
33
27
  res: null
@@ -37,40 +31,39 @@ const parseFolder = (ymlString: string): FolderRoot => {
37
31
  res: []
38
32
  },
39
33
  tests: null
40
- };
34
+ },
35
+ docs: null
36
+ };
37
+
38
+ if (ocFolder.request) {
39
+ const folderRequest = folderRoot.request!;
41
40
 
42
41
  // headers
43
42
  const headers = toBrunoHttpHeaders(ocFolder.request.headers);
44
43
  if (headers) {
45
- folderRoot.request.headers = headers;
46
- }
47
-
48
- // auth
49
- const auth = toBrunoAuth(ocFolder.request.auth);
50
- if (auth) {
51
- folderRoot.request.auth = auth;
44
+ folderRequest.headers = headers;
52
45
  }
53
46
 
54
47
  // variables
55
48
  const variables = toBrunoVariables(ocFolder.request.variables);
56
49
  const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);
57
- folderRoot.request.vars = {
50
+ folderRequest.vars = {
58
51
  req: variables.req,
59
52
  res: postResponseVars
60
53
  };
61
54
 
62
55
  // scripts
63
56
  const scripts = toBrunoScripts(ocFolder.request.scripts);
64
- if (scripts?.script && folderRoot.request.script) {
57
+ if (scripts?.script && folderRequest.script) {
65
58
  if (scripts.script.req) {
66
- folderRoot.request.script.req = scripts.script.req;
59
+ folderRequest.script.req = scripts.script.req;
67
60
  }
68
61
  if (scripts.script.res) {
69
- folderRoot.request.script.res = scripts.script.res;
62
+ folderRequest.script.res = scripts.script.res;
70
63
  }
71
64
  }
72
65
  if (scripts?.tests) {
73
- folderRoot.request.tests = scripts.tests;
66
+ folderRequest.tests = scripts.tests;
74
67
  }
75
68
  }
76
69
 
@@ -0,0 +1,147 @@
1
+ import parseItem from './parseItem';
2
+
3
+ // Typed `runtime.variables` propagate through parseItem with their dataType
4
+ // + coerced value. Helper-level coverage lives in the variables/datatype specs.
5
+
6
+ describe('parseItem — typed runtime.variables', () => {
7
+ it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
8
+ const yml = `info:
9
+ name: r
10
+ type: http
11
+
12
+ http:
13
+ url: https://example.com
14
+ method: GET
15
+
16
+ runtime:
17
+ variables:
18
+ - name: count
19
+ value:
20
+ type: number
21
+ data: "42"
22
+ - name: enabled
23
+ value:
24
+ type: boolean
25
+ data: "true"
26
+ - name: config
27
+ value:
28
+ type: object
29
+ data: '{"a":1}'
30
+ - name: greeting
31
+ value:
32
+ type: string
33
+ data: hi
34
+ - name: plain
35
+ value: hello
36
+ - name: bad
37
+ value:
38
+ type: number
39
+ data: 'not-a-number'
40
+ `;
41
+
42
+ const item = parseItem(yml);
43
+ const reqVars = item.request!.vars!.req!;
44
+
45
+ expect(reqVars).toHaveLength(6);
46
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
47
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
48
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
49
+ // Explicit `type: string` is the implicit default — no dataType materialized.
50
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
51
+ expect(reqVars[3].dataType).toBeUndefined();
52
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
53
+ expect(reqVars[4].dataType).toBeUndefined();
54
+ // Un-coercible: raw value preserved for the UI mismatch warning.
55
+ expect(reqVars[5]).toMatchObject({ name: 'bad', value: 'not-a-number', dataType: 'number' });
56
+ });
57
+
58
+ it('normalizes raw YAML scalars in `data` to a string before coercing', () => {
59
+ // YAML parses unquoted scalars per their natural JS type. The `String(... ?? '')`
60
+ // cast in fromOpenCollectionTypedValue keeps coercion consistent.
61
+ const yml = `info:
62
+ name: r
63
+ type: http
64
+
65
+ http:
66
+ url: https://example.com
67
+ method: GET
68
+
69
+ runtime:
70
+ variables:
71
+ - name: stringy
72
+ value:
73
+ type: string
74
+ data: 42
75
+ - name: numeric
76
+ value:
77
+ type: number
78
+ data: 42
79
+ - name: nullish
80
+ value:
81
+ type: number
82
+ data: null
83
+ `;
84
+
85
+ const item = parseItem(yml);
86
+ const reqVars = item.request!.vars!.req!;
87
+
88
+ // type=string: raw YAML number → string; no dataType field.
89
+ expect(reqVars[0]).toMatchObject({ name: 'stringy', value: '42' });
90
+ expect(reqVars[0].dataType).toBeUndefined();
91
+ // type=number: '42' → 42.
92
+ expect(reqVars[1]).toMatchObject({ name: 'numeric', value: 42, dataType: 'number' });
93
+ // data: null → '' (the `?? ''` arm); coerce bails on empty → raw ''.
94
+ expect(reqVars[2]).toMatchObject({ name: 'nullish', value: '', dataType: 'number' });
95
+ });
96
+
97
+ it('propagates typed variables for graphql/grpc/websocket requests too', () => {
98
+ const graphqlYml = `info:
99
+ name: g
100
+ type: graphql
101
+
102
+ graphql:
103
+ url: https://example.com/graphql
104
+ method: POST
105
+
106
+ runtime:
107
+ variables:
108
+ - name: count
109
+ value: { type: number, data: '7' }
110
+ `;
111
+ expect(parseItem(graphqlYml).request!.vars!.req![0]).toMatchObject({
112
+ name: 'count', value: 7, dataType: 'number'
113
+ });
114
+
115
+ const grpcYml = `info:
116
+ name: gr
117
+ type: grpc
118
+
119
+ grpc:
120
+ url: localhost:50051
121
+
122
+ runtime:
123
+ variables:
124
+ - name: flag
125
+ value: { type: boolean, data: 'false' }
126
+ `;
127
+ expect(parseItem(grpcYml).request!.vars!.req![0]).toMatchObject({
128
+ name: 'flag', value: false, dataType: 'boolean'
129
+ });
130
+
131
+ const wsYml = `info:
132
+ name: ws
133
+ type: websocket
134
+
135
+ websocket:
136
+ url: wss://example.com/socket
137
+
138
+ runtime:
139
+ variables:
140
+ - name: payload
141
+ value: { type: object, data: '{"k":1}' }
142
+ `;
143
+ expect(parseItem(wsYml).request!.vars!.req![0]).toMatchObject({
144
+ name: 'payload', value: { k: 1 }, dataType: 'object'
145
+ });
146
+ });
147
+ });