@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
@@ -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
+ });
@@ -10,6 +10,7 @@ import parseGraphQLRequest from './items/parseGraphQLRequest';
10
10
  import parseGrpcRequest from './items/parseGrpcRequest';
11
11
  import parseWebsocketRequest from './items/parseWebsocketRequest';
12
12
  import parseScript from './items/parseScript';
13
+ import parseApp, { type AppFile } from './items/parseApp';
13
14
 
14
15
  // Helper to get the type from an item (now in info block)
15
16
  const getItemType = (item: Item): string | undefined => {
@@ -87,6 +88,9 @@ const parseItem = (ymlString: string): BrunoItem => {
87
88
  case 'script':
88
89
  return parseScript(ocItem as ScriptFile);
89
90
 
91
+ case 'app':
92
+ return parseApp(ocItem as unknown as AppFile);
93
+
90
94
  case 'folder':
91
95
  throw new Error('Folder items should be handled separately using parseFolder');
92
96
 
@@ -0,0 +1,77 @@
1
+ import stringifyCollection from './stringifyCollection';
2
+ import parseCollection from './parseCollection';
3
+
4
+ // Typed collection-root vars serialize to OC's `{ type, data }` struct.
5
+ // `dataType: 'string'` is the implicit default and stays a raw string.
6
+
7
+ describe('stringifyCollection — typed request.variables', () => {
8
+ it('round-trips typed values and omits a typed struct for the implicit string default', () => {
9
+ const collectionRoot = {
10
+ meta: null,
11
+ request: {
12
+ headers: [],
13
+ auth: { mode: 'none' },
14
+ script: { req: null, res: null },
15
+ tests: null,
16
+ vars: {
17
+ req: [
18
+ { uid: 'v1', name: 'count', value: 42, enabled: true, dataType: 'number' },
19
+ { uid: 'v2', name: 'enabled', value: true, enabled: true, dataType: 'boolean' },
20
+ { uid: 'v3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' },
21
+ { uid: 'v4', name: 'greeting', value: 'hi', enabled: true, dataType: 'string' },
22
+ { uid: 'v5', name: 'plain', value: 'hello', enabled: true }
23
+ ],
24
+ res: []
25
+ }
26
+ },
27
+ docs: null
28
+ } as any;
29
+
30
+ const brunoConfig = { name: 'c' };
31
+
32
+ const yml = stringifyCollection(collectionRoot, brunoConfig);
33
+
34
+ // `type: string` is never written out.
35
+ expect(yml).not.toMatch(/type:\s*string/);
36
+
37
+ const { collectionRoot: reparsed } = parseCollection(yml);
38
+ const reqVars = reparsed.request!.vars!.req!;
39
+
40
+ expect(reqVars).toHaveLength(5);
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
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
45
+ expect(reqVars[3].dataType).toBeUndefined();
46
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
47
+ expect(reqVars[4].dataType).toBeUndefined();
48
+ });
49
+ });
50
+
51
+ describe('stringifyCollection — writing the collection version', () => {
52
+ const baseRoot = {
53
+ meta: null,
54
+ request: { headers: [], auth: { mode: 'none' }, script: { req: null, res: null }, tests: null, vars: { req: [], res: [] } },
55
+ docs: null
56
+ } as any;
57
+
58
+ it('writes the version as-is and reads back the same value', () => {
59
+ const yml = stringifyCollection(baseRoot, { name: 'c', version: '2' });
60
+ expect(yml).toMatch(/version:\s*['"]?2['"]?/);
61
+ expect(parseCollection(yml).brunoConfig.version).toBe('2');
62
+ });
63
+
64
+ it('writes no version when the collection has none', () => {
65
+ const yml = stringifyCollection(baseRoot, { name: 'c' });
66
+ expect(parseCollection(yml).brunoConfig.version).toBeUndefined();
67
+ });
68
+
69
+ it('accepts any version text — there is no check on the format', () => {
70
+ // The version is free-form: users can set whatever they like. Each of these should be
71
+ // saved and read back exactly the same, without changing the format or rejecting it.
72
+ ['v1.0.0', '2', '1.2.3-beta', '2024.01.15', 'release-2024-Q3', 'anything-goes'].forEach((version) => {
73
+ const yml = stringifyCollection(baseRoot, { name: 'c', version });
74
+ expect(parseCollection(yml).brunoConfig.version).toBe(version);
75
+ });
76
+ });
77
+ });
@@ -73,6 +73,10 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
73
73
  name: brunoConfig.name || 'Untitled Collection'
74
74
  };
75
75
 
76
+ if (brunoConfig.version != null && brunoConfig.version !== '') {
77
+ oc.info.version = String(brunoConfig.version);
78
+ }
79
+
76
80
  // collection config
77
81
  if (hasCollectionConfig(brunoConfig)) {
78
82
  oc.config = {};
@@ -0,0 +1,91 @@
1
+ import stringifyEnvironment, { toOpenCollectionEnvironmentVariables } from './stringifyEnvironment';
2
+ import parseEnvironment from './parseEnvironment';
3
+
4
+ describe('toOpenCollectionEnvironmentVariables', () => {
5
+ it('returns undefined for null / empty input', () => {
6
+ expect(toOpenCollectionEnvironmentVariables([])).toBeUndefined();
7
+ });
8
+
9
+ it('serializes plain string variables as raw strings', () => {
10
+ const out = toOpenCollectionEnvironmentVariables([
11
+ { uid: 'u1', name: 'apiKey', value: 'abc', type: 'text', enabled: true, secret: false } as any
12
+ ]);
13
+
14
+ expect(out).toEqual([{ name: 'apiKey', value: 'abc' }]);
15
+ });
16
+
17
+ it('serializes typed variables as a {type, data} struct', () => {
18
+ const out = toOpenCollectionEnvironmentVariables([
19
+ { uid: 'u1', name: 'port', value: 300, type: 'text', enabled: true, secret: false, dataType: 'number' } as any,
20
+ { uid: 'u2', name: 'flag', value: true, type: 'text', enabled: true, secret: false, dataType: 'boolean' } as any,
21
+ { uid: 'u3', name: 'config', value: { a: 1 }, type: 'text', enabled: true, secret: false, dataType: 'object' } as any
22
+ ]);
23
+
24
+ expect(out).toEqual([
25
+ { name: 'port', value: { type: 'number', data: '300' } },
26
+ { name: 'flag', value: { type: 'boolean', data: 'true' } },
27
+ { name: 'config', value: { type: 'object', data: '{\n "a": 1\n}' } }
28
+ ]);
29
+ });
30
+
31
+ it('serializes secret variables without a value but preserves the dataType as `type`', () => {
32
+ const out = toOpenCollectionEnvironmentVariables([
33
+ { uid: 'u1', name: 'apiKey', value: '', type: 'text', enabled: true, secret: true } as any,
34
+ { uid: 'u2', name: 'flag', value: '', type: 'text', enabled: false, secret: true, dataType: 'number' } as any
35
+ ]);
36
+
37
+ expect(out).toEqual([
38
+ { secret: true, name: 'apiKey' },
39
+ { secret: true, name: 'flag', type: 'number', disabled: true }
40
+ ]);
41
+ });
42
+
43
+ it('marks disabled non-secret variables', () => {
44
+ const out = toOpenCollectionEnvironmentVariables([
45
+ { uid: 'u1', name: 'apiKey', value: 'abc', type: 'text', enabled: false, secret: false } as any
46
+ ]);
47
+
48
+ expect(out).toEqual([{ name: 'apiKey', value: 'abc', disabled: true }]);
49
+ });
50
+ });
51
+
52
+ describe('stringifyEnvironment', () => {
53
+ it('round-trips through parseEnvironment for typed and secret variables', () => {
54
+ const env = {
55
+ uid: 'env-uid',
56
+ name: 'test_env',
57
+ variables: [
58
+ { uid: 'u1', name: 'env_str', value: 'hello', type: 'text', enabled: true, secret: false },
59
+ { uid: 'u2', name: 'env_num', value: 300, type: 'text', enabled: true, secret: false, dataType: 'number' as const },
60
+ { uid: 'u3', name: 'env_bool', value: true, type: 'text', enabled: true, secret: false, dataType: 'boolean' as const },
61
+ { uid: 'u4', name: 'env_obj', value: { scope: 'env' }, type: 'text', enabled: true, secret: false, dataType: 'object' as const },
62
+ { uid: 'u5', name: 'env_secret', value: '', type: 'text', enabled: true, secret: true },
63
+ { uid: 'u6', name: 'env_secret_num', value: '', type: 'text', enabled: true, secret: true, dataType: 'number' as const }
64
+ ]
65
+ } as any;
66
+
67
+ const yml = stringifyEnvironment(env);
68
+ const reparsed = parseEnvironment(yml);
69
+
70
+ expect(reparsed.name).toBe('test_env');
71
+ expect(reparsed.variables[0]).toMatchObject({ name: 'env_str', value: 'hello', secret: false });
72
+ expect(reparsed.variables[1]).toMatchObject({ name: 'env_num', value: 300, dataType: 'number', secret: false });
73
+ expect(reparsed.variables[2]).toMatchObject({ name: 'env_bool', value: true, dataType: 'boolean', secret: false });
74
+ expect(reparsed.variables[3]).toMatchObject({ name: 'env_obj', value: { scope: 'env' }, dataType: 'object', secret: false });
75
+ expect(reparsed.variables[4]).toMatchObject({ name: 'env_secret', secret: true, value: '' });
76
+ expect(reparsed.variables[4].dataType).toBeUndefined();
77
+ expect(reparsed.variables[5]).toMatchObject({ name: 'env_secret_num', secret: true, value: '', dataType: 'number' });
78
+ });
79
+
80
+ it('preserves the color field when present', () => {
81
+ const env = {
82
+ uid: 'env-uid',
83
+ name: 'colorful',
84
+ color: '#ff0000',
85
+ variables: []
86
+ } as any;
87
+
88
+ const reparsed = parseEnvironment(stringifyEnvironment(env));
89
+ expect(reparsed.color).toBe('#ff0000');
90
+ });
91
+ });
@@ -1,53 +1,89 @@
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 { stringifyYml } from './utils';
5
10
  import { ensureString } from '../../utils';
11
+ import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
12
+ import { hasTypedMetadata, toOpenCollectionTypedValue, serializeVariableValue } from './common/datatype';
6
13
 
7
- const toOpenCollectionEnvironmentVariables = (variables: BrunoEnvironmentVariable[]): (Variable | SecretVariable)[] | undefined => {
14
+ export const toOpenCollectionEnvironmentVariables = (variables: (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[]): (Variable | SecretVariable)[] | undefined => {
8
15
  if (!variables?.length) {
9
16
  return undefined;
10
17
  }
11
18
 
12
19
  const ocVariables: (Variable | SecretVariable)[] = variables
13
- .filter((v: BrunoEnvironmentVariable) => {
14
- // todo: currently neither bru lang nor bruno app supports non-string values
15
- // update this when bruno app supports non-string values
16
- return typeof v.value === 'string';
17
- })
18
- .map((v: BrunoEnvironmentVariable): Variable | SecretVariable => {
19
- if (v.secret === true) {
20
+ .map((v): Variable | SecretVariable => {
21
+ if ('secret' in v && v.secret === true) {
20
22
  const secretVar: SecretVariable = {
21
23
  secret: true,
22
24
  name: v.name || ''
23
25
  };
24
- if (v.datatype && v.datatype !== 'string') {
25
- secretVar.type = v.datatype;
26
+ if (hasTypedMetadata(v)) {
27
+ secretVar.type = v.dataType;
26
28
  }
27
29
  if (v.enabled === false) {
28
30
  secretVar.disabled = true;
29
31
  }
32
+ if (v.description !== undefined) {
33
+ secretVar.description = v.description;
34
+ }
30
35
  return secretVar;
31
36
  }
32
37
 
38
+ const valueStr = serializeVariableValue(v.value);
39
+
33
40
  const variable: Variable = {
34
41
  name: v.name || '',
35
- value:
36
- v.datatype && v.datatype !== 'string'
37
- ? { type: v.datatype, data: ensureString(v.value) }
38
- : ensureString(v.value)
42
+ value: hasTypedMetadata(v) ? toOpenCollectionTypedValue(v, valueStr) : valueStr
39
43
  };
40
44
 
41
45
  if (v.enabled === false) {
42
46
  variable.disabled = true;
43
47
  }
44
48
 
49
+ if (v.description !== undefined) {
50
+ variable.description = v.description;
51
+ }
52
+
45
53
  return variable;
46
54
  });
47
55
 
48
56
  return ocVariables.length > 0 ? ocVariables : undefined;
49
57
  };
50
58
 
59
+ // Serializes the runtime externalSecrets block to yml `{ type, variables: [{ name,
60
+ // path|secretName|vaultName, disabled? }] }`. Runtime-only fields (uid, value,
61
+ // providerConfig, lastSyncedAt) are never written.
62
+ const toOpenCollectionExternalSecrets = (externalSecrets: BrunoEnvironment['externalSecrets']): { type: string; variables: Record<string, string | boolean>[] } | undefined => {
63
+ const type = externalSecrets?.type as BrunoExternalSecretProviderType | undefined;
64
+ const identifierField = type && SECRET_PROVIDER_IDENTIFIER_FIELDS[type];
65
+ if (!type || !identifierField) {
66
+ return undefined;
67
+ }
68
+
69
+ // Keep any named entry, even one with an empty identifier — a half-filled secret row is the user's
70
+ // data; dropping it here would silently delete it on the next save.
71
+ const variables = (externalSecrets?.variables || [])
72
+ .filter((v: any) => v && v.name)
73
+ .map((v: any) => {
74
+ const secret: Record<string, string | boolean> = {
75
+ name: v.name,
76
+ [identifierField]: ensureString(v[identifierField])
77
+ };
78
+ if (v.enabled === false) {
79
+ secret.disabled = true;
80
+ }
81
+ return secret;
82
+ });
83
+
84
+ return { type, variables };
85
+ };
86
+
51
87
  const stringifyEnvironment = (environment: BrunoEnvironment): string => {
52
88
  try {
53
89
  const ocEnvironment: Environment = {
@@ -65,11 +101,9 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => {
65
101
  }
66
102
  }
67
103
 
68
- if (environment.externalSecrets) {
69
- (ocEnvironment as any).externalSecrets = {
70
- type: environment.externalSecrets.type,
71
- variables: (environment.externalSecrets.variables || []).map((variable) => ({ ...variable }))
72
- };
104
+ const ocExternalSecrets = toOpenCollectionExternalSecrets(environment.externalSecrets);
105
+ if (ocExternalSecrets) {
106
+ (ocEnvironment as any).externalSecrets = ocExternalSecrets;
73
107
  }
74
108
 
75
109
  return stringifyYml(ocEnvironment);
@@ -78,4 +112,5 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => {
78
112
  throw error;
79
113
  }
80
114
  };
115
+
81
116
  export default stringifyEnvironment;
@@ -0,0 +1,47 @@
1
+ import stringifyFolder from './stringifyFolder';
2
+ import parseFolder from './parseFolder';
3
+
4
+ // Typed folder vars serialize to OC's `{ type, data }` struct.
5
+ // `dataType: 'string'` is the implicit default and stays a raw string.
6
+
7
+ describe('stringifyFolder — typed request.variables', () => {
8
+ it('round-trips typed values and omits a typed struct for the implicit string default', () => {
9
+ const folderRoot = {
10
+ meta: { name: 'my-folder', seq: 1 },
11
+ request: {
12
+ headers: [],
13
+ auth: { mode: 'none' },
14
+ script: { req: null, res: null },
15
+ tests: null,
16
+ vars: {
17
+ req: [
18
+ { uid: 'v1', name: 'count', value: 42, enabled: true, dataType: 'number' },
19
+ { uid: 'v2', name: 'enabled', value: true, enabled: true, dataType: 'boolean' },
20
+ { uid: 'v3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' },
21
+ { uid: 'v4', name: 'greeting', value: 'hi', enabled: true, dataType: 'string' },
22
+ { uid: 'v5', name: 'plain', value: 'hello', enabled: true }
23
+ ],
24
+ res: []
25
+ }
26
+ },
27
+ docs: null
28
+ } as any;
29
+
30
+ const yml = stringifyFolder(folderRoot);
31
+
32
+ // `type: string` is never written out.
33
+ expect(yml).not.toMatch(/type:\s*string/);
34
+
35
+ const reparsed = parseFolder(yml);
36
+ const reqVars = reparsed.request!.vars!.req!;
37
+
38
+ expect(reqVars).toHaveLength(5);
39
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
40
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
41
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
42
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
43
+ expect(reqVars[3].dataType).toBeUndefined();
44
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
45
+ expect(reqVars[4].dataType).toBeUndefined();
46
+ });
47
+ });
@@ -0,0 +1,53 @@
1
+ import stringifyItem from './stringifyItem';
2
+ import parseItem from './parseItem';
3
+
4
+ // Typed bruno vars on `request.vars.req` serialize to OC's `{ type, data }`
5
+ // struct. `dataType: 'string'` is the implicit default and stays a raw string.
6
+
7
+ describe('stringifyItem — typed runtime.variables', () => {
8
+ it('round-trips typed values and omits a typed struct for the implicit string default', () => {
9
+ const item = {
10
+ uid: 'i1',
11
+ type: 'http-request',
12
+ name: 'r',
13
+ seq: 1,
14
+ request: {
15
+ url: 'https://example.com',
16
+ method: 'GET',
17
+ headers: [],
18
+ params: [],
19
+ body: { mode: 'none' },
20
+ auth: { mode: 'none' },
21
+ script: { req: null, res: null },
22
+ tests: null,
23
+ vars: {
24
+ req: [
25
+ { uid: 'v1', name: 'count', value: 42, enabled: true, dataType: 'number' },
26
+ { uid: 'v2', name: 'enabled', value: true, enabled: true, dataType: 'boolean' },
27
+ { uid: 'v3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' },
28
+ { uid: 'v4', name: 'greeting', value: 'hi', enabled: true, dataType: 'string' },
29
+ { uid: 'v5', name: 'plain', value: 'hello', enabled: true }
30
+ ],
31
+ res: []
32
+ }
33
+ }
34
+ } as any;
35
+
36
+ const yml = stringifyItem(item);
37
+
38
+ // `type: string` is never written out.
39
+ expect(yml).not.toMatch(/type:\s*string/);
40
+
41
+ const reparsed = parseItem(yml);
42
+ const reqVars = reparsed.request!.vars!.req!;
43
+
44
+ expect(reqVars).toHaveLength(5);
45
+ expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
46
+ expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
47
+ expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
48
+ expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
49
+ expect(reqVars[3].dataType).toBeUndefined();
50
+ expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
51
+ expect(reqVars[4].dataType).toBeUndefined();
52
+ });
53
+ });
@@ -4,6 +4,7 @@ import stringifyGraphqlRequest from './items/stringifyGraphQLRequest';
4
4
  import stringifyGrpcRequest from './items/stringifyGrpcRequest';
5
5
  import stringifyWebsocketRequest from './items/stringifyWebsocketRequest';
6
6
  import stringifyScript from './items/stringifyScript';
7
+ import stringifyApp from './items/stringifyApp';
7
8
 
8
9
  const stringifyItem = (item: BrunoItem): string => {
9
10
  try {
@@ -23,6 +24,9 @@ const stringifyItem = (item: BrunoItem): string => {
23
24
  case 'js':
24
25
  return stringifyScript(item);
25
26
 
27
+ case 'app':
28
+ return stringifyApp(item);
29
+
26
30
  case 'folder':
27
31
  throw new Error('Folder items should be handled separately using stringifyFolder');
28
32
 
@@ -0,0 +1,7 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
4
+ extensions:
5
+ bruno:
6
+ presets:
7
+ request: {}
@@ -0,0 +1,3 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
@@ -0,0 +1,6 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
4
+ extensions:
5
+ bruno:
6
+ presets: {}
@@ -0,0 +1,24 @@
1
+ opencollection: 1.0.0
2
+
3
+ info:
4
+ name: md8
5
+ config:
6
+ proxy:
7
+ inherit: true
8
+ config:
9
+ protocol: http
10
+ hostname: ""
11
+ port: ""
12
+ auth:
13
+ username: ""
14
+ password: ""
15
+ bypassProxy: ""
16
+ bundled: false
17
+ extensions:
18
+ bruno:
19
+ ignore:
20
+ - node_modules
21
+ - .git
22
+ presets:
23
+ request:
24
+ type: graphql
@@ -0,0 +1,25 @@
1
+ opencollection: 1.0.0
2
+
3
+ info:
4
+ name: md8
5
+ config:
6
+ proxy:
7
+ inherit: true
8
+ config:
9
+ protocol: http
10
+ hostname: ""
11
+ port: ""
12
+ auth:
13
+ username: ""
14
+ password: ""
15
+ bypassProxy: ""
16
+ bundled: false
17
+ extensions:
18
+ bruno:
19
+ ignore:
20
+ - node_modules
21
+ - .git
22
+ presets:
23
+ request:
24
+ type: http
25
+ url: https://example.com/graphql
@@ -0,0 +1,9 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
4
+ extensions:
5
+ bruno:
6
+ presets:
7
+ request:
8
+ type: graphql
9
+ url: https://example.com/graphql