@usebruno/filestore 0.10.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 (118) 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 +1 -2
  22. package/dist/formats/yml/common/variables.d.ts.map +1 -1
  23. package/dist/formats/yml/items/parseApp.d.ts +13 -0
  24. package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
  25. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  26. package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
  27. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  28. package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
  29. package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
  30. package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
  31. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  32. package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
  33. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  34. package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
  35. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  36. package/dist/formats/yml/parseEnvironment.d.ts +3 -1
  37. package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
  38. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  39. package/dist/formats/yml/parseItem.d.ts.map +1 -1
  40. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  41. package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
  42. package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
  43. package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/types.d.ts +6 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
  49. package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
  50. package/package.json +7 -5
  51. package/src/constants.ts +9 -0
  52. package/src/formats/bru/index.ts +52 -0
  53. package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
  54. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +313 -0
  55. package/src/formats/bru/utils/redact-large-text-blocks.ts +102 -0
  56. package/src/formats/yml/common/actions.ts +1 -1
  57. package/src/formats/yml/common/app.ts +27 -0
  58. package/src/formats/yml/common/auth.ts +53 -1
  59. package/src/formats/yml/common/body.spec.ts +62 -0
  60. package/src/formats/yml/common/body.ts +27 -9
  61. package/src/formats/yml/common/datatype.spec.ts +110 -0
  62. package/src/formats/yml/common/datatype.ts +57 -0
  63. package/src/formats/yml/common/headers.ts +4 -0
  64. package/src/formats/yml/common/variables.spec.ts +127 -0
  65. package/src/formats/yml/common/variables.ts +14 -22
  66. package/src/formats/yml/items/parseApp.ts +37 -0
  67. package/src/formats/yml/items/parseGraphQLRequest.ts +8 -0
  68. package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
  69. package/src/formats/yml/items/parseHttpRequest.ts +13 -0
  70. package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
  71. package/src/formats/yml/items/stringifyApp.ts +45 -0
  72. package/src/formats/yml/items/stringifyGraphQLRequest.ts +3 -0
  73. package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
  74. package/src/formats/yml/items/stringifyHttpRequest.ts +10 -0
  75. package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
  76. package/src/formats/yml/parseCollection.spec.ts +70 -0
  77. package/src/formats/yml/parseCollection.ts +8 -3
  78. package/src/formats/yml/parseEnvironment.spec.ts +99 -0
  79. package/src/formats/yml/parseEnvironment.ts +41 -19
  80. package/src/formats/yml/parseFolder.spec.ts +52 -0
  81. package/src/formats/yml/parseFolder.ts +14 -21
  82. package/src/formats/yml/parseItem.spec.ts +147 -0
  83. package/src/formats/yml/parseItem.ts +4 -0
  84. package/src/formats/yml/stringifyCollection.spec.ts +77 -0
  85. package/src/formats/yml/stringifyCollection.ts +4 -0
  86. package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
  87. package/src/formats/yml/stringifyEnvironment.ts +55 -20
  88. package/src/formats/yml/stringifyFolder.spec.ts +47 -0
  89. package/src/formats/yml/stringifyItem.spec.ts +53 -0
  90. package/src/formats/yml/stringifyItem.ts +4 -0
  91. package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
  92. package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
  93. package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
  94. package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
  95. package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
  96. package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
  97. package/src/formats/yml/tests/parseCollection.spec.js +63 -0
  98. package/src/formats/yml/tests/parseEnvironment.spec.js +24 -63
  99. package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
  100. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +204 -0
  101. package/src/formats/yml/tests/variables.spec.js +7 -7
  102. package/src/index.ts +5 -0
  103. package/src/types/bruno-lang.d.ts +1 -0
  104. package/src/types.ts +7 -0
  105. package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
  106. package/src/utils/secretsJsonToEnvironments.ts +129 -0
  107. package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
  108. package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
  109. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
  110. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
  111. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
  112. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
  113. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
  114. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
  115. package/dist/formats/yml/tests/parseEnvironment.spec.d.ts +0 -2
  116. package/dist/formats/yml/tests/parseEnvironment.spec.d.ts.map +0 -1
  117. package/dist/formats/yml/tests/variables.spec.d.ts +0 -2
  118. package/dist/formats/yml/tests/variables.spec.d.ts.map +0 -1
@@ -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
@@ -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,20 +1,26 @@
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';
6
- import { isTypedValue } from './common/variables';
11
+ import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
12
+ import { isTypedValue, fromOpenCollectionTypedValue } from './common/datatype';
7
13
 
8
14
  const isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {
9
15
  return 'secret' in v && v.secret === true;
10
16
  };
11
17
 
12
- const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): BrunoEnvironmentVariable[] => {
18
+ export const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[] => {
13
19
  if (!variables?.length) {
14
20
  return [];
15
21
  }
16
22
 
17
- return variables.map((v): BrunoEnvironmentVariable => {
23
+ return variables.map((v): (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable) => {
18
24
  if (isSecretVariable(v)) {
19
25
  const variable: BrunoEnvironmentVariable = {
20
26
  uid: uuid(),
@@ -26,11 +32,16 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
26
32
  };
27
33
 
28
34
  if (v.type && v.type !== 'string' && v.type !== 'null') {
29
- variable.datatype = v.type;
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 || '';
30
40
  }
31
41
 
32
42
  return variable;
33
43
  }
44
+
34
45
  const variable: BrunoEnvironmentVariable = {
35
46
  uid: uuid(),
36
47
  name: ensureString(v.name),
@@ -41,36 +52,47 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
41
52
  };
42
53
 
43
54
  if (isTypedValue(v.value)) {
44
- variable.value = ensureString(v.value.data);
45
- if (v.value.type !== 'string' && v.value.type !== 'null') {
46
- variable.datatype = v.value.type;
47
- }
55
+ Object.assign(variable, fromOpenCollectionTypedValue(v.value));
48
56
  } else {
49
57
  variable.value = ensureString(v.value);
50
58
  }
51
59
 
60
+ if (v.description) {
61
+ variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
62
+ }
63
+
52
64
  return variable;
53
65
  });
54
66
  };
55
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.
56
71
  const toBrunoExternalSecrets = (externalSecrets: any): BrunoEnvironment['externalSecrets'] | undefined => {
57
72
  if (!externalSecrets || typeof externalSecrets !== 'object') {
58
73
  return undefined;
59
74
  }
60
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.
61
84
  const variables = Array.isArray(externalSecrets.variables)
62
- ? externalSecrets.variables.map((variable: any) => {
63
- const result: Record<string, string> = { name: ensureString(variable?.name) };
64
- Object.keys(variable || {}).forEach((key) => {
65
- if (key !== 'name') {
66
- result[key] = ensureString(variable[key]);
67
- }
68
- });
69
- return result;
70
- })
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
+ }))
71
93
  : [];
72
94
 
73
- return { type: ensureString(externalSecrets.type), variables } as BrunoEnvironment['externalSecrets'];
95
+ return { type, variables } as BrunoEnvironment['externalSecrets'];
74
96
  };
75
97
 
76
98
  const parseEnvironment = (ymlString: string): BrunoEnvironment => {
@@ -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