@usebruno/filestore 0.10.0 → 0.12.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 (143) 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/index.d.ts +3 -1
  24. package/dist/formats/yml/index.d.ts.map +1 -1
  25. package/dist/formats/yml/items/parseApp.d.ts +13 -0
  26. package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
  27. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  28. package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
  29. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  30. package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
  31. package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
  32. package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
  33. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  34. package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
  35. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  36. package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
  37. package/dist/formats/yml/mockServerTypes.d.ts +95 -0
  38. package/dist/formats/yml/mockServerTypes.d.ts.map +1 -0
  39. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  40. package/dist/formats/yml/parseEnvironment.d.ts +3 -1
  41. package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
  42. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  43. package/dist/formats/yml/parseItem.d.ts.map +1 -1
  44. package/dist/formats/yml/parseMockServer.d.ts +4 -0
  45. package/dist/formats/yml/parseMockServer.d.ts.map +1 -0
  46. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  47. package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
  48. package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
  49. package/dist/formats/yml/stringifyFolder.d.ts.map +1 -1
  50. package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
  51. package/dist/formats/yml/stringifyMockServer.d.ts +4 -0
  52. package/dist/formats/yml/stringifyMockServer.d.ts.map +1 -0
  53. package/dist/index.d.ts +18 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/types.d.ts +7 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
  58. package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
  59. package/dist/workers/WorkerQueue/index.d.ts +3 -2
  60. package/dist/workers/WorkerQueue/index.d.ts.map +1 -1
  61. package/dist/workers/index.d.ts +4 -0
  62. package/dist/workers/index.d.ts.map +1 -1
  63. package/package.json +7 -5
  64. package/src/constants.ts +13 -0
  65. package/src/formats/bru/index.ts +52 -0
  66. package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
  67. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +472 -0
  68. package/src/formats/bru/utils/redact-large-text-blocks.ts +143 -0
  69. package/src/formats/yml/common/actions.ts +1 -1
  70. package/src/formats/yml/common/app.ts +27 -0
  71. package/src/formats/yml/common/auth.ts +53 -1
  72. package/src/formats/yml/common/body.spec.ts +62 -0
  73. package/src/formats/yml/common/body.ts +27 -9
  74. package/src/formats/yml/common/datatype.spec.ts +110 -0
  75. package/src/formats/yml/common/datatype.ts +57 -0
  76. package/src/formats/yml/common/headers.ts +4 -0
  77. package/src/formats/yml/common/variables.spec.ts +127 -0
  78. package/src/formats/yml/common/variables.ts +14 -22
  79. package/src/formats/yml/index.ts +5 -1
  80. package/src/formats/yml/items/parseApp.ts +37 -0
  81. package/src/formats/yml/items/parseGraphQLRequest.ts +14 -16
  82. package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
  83. package/src/formats/yml/items/parseHttpRequest.ts +19 -16
  84. package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
  85. package/src/formats/yml/items/stringifyApp.ts +45 -0
  86. package/src/formats/yml/items/stringifyGraphQLRequest.ts +7 -6
  87. package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
  88. package/src/formats/yml/items/stringifyHttpRequest.ts +14 -6
  89. package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
  90. package/src/formats/yml/mockServerTypes.ts +95 -0
  91. package/src/formats/yml/parseCollection.spec.ts +193 -0
  92. package/src/formats/yml/parseCollection.ts +24 -6
  93. package/src/formats/yml/parseEnvironment.spec.ts +99 -0
  94. package/src/formats/yml/parseEnvironment.ts +41 -19
  95. package/src/formats/yml/parseFolder.spec.ts +70 -0
  96. package/src/formats/yml/parseFolder.ts +19 -22
  97. package/src/formats/yml/parseItem.spec.ts +147 -0
  98. package/src/formats/yml/parseItem.ts +4 -0
  99. package/src/formats/yml/parseMockServer.spec.ts +180 -0
  100. package/src/formats/yml/parseMockServer.ts +113 -0
  101. package/src/formats/yml/stringifyCollection.spec.ts +223 -0
  102. package/src/formats/yml/stringifyCollection.ts +26 -9
  103. package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
  104. package/src/formats/yml/stringifyEnvironment.ts +55 -20
  105. package/src/formats/yml/stringifyFolder.spec.ts +70 -0
  106. package/src/formats/yml/stringifyFolder.ts +7 -2
  107. package/src/formats/yml/stringifyItem.spec.ts +53 -0
  108. package/src/formats/yml/stringifyItem.ts +4 -0
  109. package/src/formats/yml/stringifyMockServer.spec.ts +177 -0
  110. package/src/formats/yml/stringifyMockServer.ts +129 -0
  111. package/src/formats/yml/tests/defaultEnvironment.spec.js +50 -0
  112. package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
  113. package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
  114. package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
  115. package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
  116. package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
  117. package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
  118. package/src/formats/yml/tests/parseCollection.spec.js +63 -0
  119. package/src/formats/yml/tests/parseEnvironment.spec.js +24 -63
  120. package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
  121. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +230 -0
  122. package/src/formats/yml/tests/variables.spec.js +7 -7
  123. package/src/formats/yml/utils.ts +1 -1
  124. package/src/index.ts +39 -1
  125. package/src/types/bruno-lang.d.ts +1 -0
  126. package/src/types.ts +8 -0
  127. package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
  128. package/src/utils/secretsJsonToEnvironments.ts +129 -0
  129. package/src/workers/WorkerQueue/index.ts +10 -2
  130. package/src/workers/index.ts +25 -1
  131. package/src/workers/worker-script.ts +45 -21
  132. package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
  133. package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
  134. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
  135. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
  136. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
  137. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
  138. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
  139. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
  140. package/dist/formats/yml/tests/parseEnvironment.spec.d.ts +0 -2
  141. package/dist/formats/yml/tests/parseEnvironment.spec.d.ts.map +0 -1
  142. package/dist/formats/yml/tests/variables.spec.d.ts +0 -2
  143. package/dist/formats/yml/tests/variables.spec.d.ts.map +0 -1
@@ -0,0 +1,223 @@
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 — script execution flow', () => {
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('round-trips flow: sequential through the bruno extension', () => {
59
+ const yml = stringifyCollection(baseRoot, { name: 'c', scripts: { flow: 'sequential' } });
60
+ expect(yml).toMatch(/flow:\s*sequential/);
61
+ expect(parseCollection(yml).brunoConfig.scripts?.flow).toBe('sequential');
62
+ });
63
+
64
+ it('round-trips flow: sandwich through the bruno extension', () => {
65
+ const yml = stringifyCollection(baseRoot, { name: 'c', scripts: { flow: 'sandwich' } });
66
+ expect(parseCollection(yml).brunoConfig.scripts?.flow).toBe('sandwich');
67
+ });
68
+
69
+ it('writes no flow when the collection has none', () => {
70
+ const yml = stringifyCollection(baseRoot, { name: 'c' });
71
+ expect(yml).not.toMatch(/flow:/);
72
+ expect(parseCollection(yml).brunoConfig.scripts?.flow).toBeUndefined();
73
+ });
74
+
75
+ it('drops an unrecognized flow value on write', () => {
76
+ const yml = stringifyCollection(baseRoot, { name: 'c', scripts: { flow: 'parallel' } });
77
+ expect(yml).not.toMatch(/flow:/);
78
+ });
79
+
80
+ it('preserves additionalContextRoots alongside a written flow value', () => {
81
+ const yml = stringifyCollection(baseRoot, {
82
+ name: 'c',
83
+ scripts: { flow: 'sequential', additionalContextRoots: ['../libs'] }
84
+ });
85
+ const { brunoConfig } = parseCollection(yml);
86
+ expect(brunoConfig.scripts?.flow).toBe('sequential');
87
+ expect(brunoConfig.scripts?.additionalContextRoots).toEqual(['../libs']);
88
+ });
89
+ });
90
+
91
+ describe('stringifyCollection — client certificates', () => {
92
+ const baseRoot = {
93
+ meta: null,
94
+ request: { headers: [], auth: { mode: 'none' }, script: { req: null, res: null }, tests: null, vars: { req: [], res: [] } },
95
+ docs: null
96
+ } as any;
97
+
98
+ it('round-trips cert/pfx certificates', () => {
99
+ const brunoConfig = {
100
+ name: 'c',
101
+ clientCertificates: {
102
+ certs: [
103
+ {
104
+ domain: 'localhost',
105
+ type: 'cert',
106
+ certFilePath: './certs/client-cert.pem',
107
+ keyFilePath: './certs/client-key.pem',
108
+ passphrase: 'secret'
109
+ },
110
+ {
111
+ domain: 'example.com',
112
+ type: 'pfx',
113
+ pfxFilePath: './certs/client.pfx',
114
+ passphrase: ''
115
+ }
116
+ ]
117
+ }
118
+ };
119
+
120
+ const yml = stringifyCollection(baseRoot, brunoConfig);
121
+ const { brunoConfig: reparsed } = parseCollection(yml);
122
+
123
+ expect(reparsed.clientCertificates.certs).toEqual([
124
+ {
125
+ domain: 'localhost',
126
+ type: 'cert',
127
+ certFilePath: './certs/client-cert.pem',
128
+ keyFilePath: './certs/client-key.pem',
129
+ passphrase: 'secret'
130
+ },
131
+ {
132
+ domain: 'example.com',
133
+ type: 'pfx',
134
+ pfxFilePath: './certs/client.pfx',
135
+ passphrase: ''
136
+ }
137
+ ]);
138
+ });
139
+
140
+ it('writes disabled: true for disabled certs and round-trips it', () => {
141
+ const brunoConfig = {
142
+ name: 'c',
143
+ clientCertificates: {
144
+ certs: [
145
+ {
146
+ domain: 'localhost',
147
+ type: 'cert',
148
+ certFilePath: './certs/client-cert.pem',
149
+ keyFilePath: './certs/client-key.pem',
150
+ passphrase: 'secret',
151
+ disabled: true
152
+ },
153
+ {
154
+ domain: 'example.com',
155
+ type: 'pfx',
156
+ pfxFilePath: './certs/client.pfx',
157
+ passphrase: '',
158
+ disabled: true
159
+ }
160
+ ]
161
+ }
162
+ };
163
+
164
+ const yml = stringifyCollection(baseRoot, brunoConfig);
165
+
166
+ expect(yml).toMatch(/disabled:\s*true/);
167
+
168
+ const { brunoConfig: reparsed } = parseCollection(yml);
169
+ expect(reparsed.clientCertificates.certs[0].disabled).toBe(true);
170
+ expect(reparsed.clientCertificates.certs[1].disabled).toBe(true);
171
+ });
172
+
173
+ it('does not write disabled for enabled certs', () => {
174
+ const brunoConfig = {
175
+ name: 'c',
176
+ clientCertificates: {
177
+ certs: [
178
+ {
179
+ domain: 'example.com',
180
+ type: 'pfx',
181
+ pfxFilePath: './certs/client.pfx',
182
+ passphrase: ''
183
+ }
184
+ ]
185
+ }
186
+ };
187
+
188
+ const yml = stringifyCollection(baseRoot, brunoConfig);
189
+
190
+ expect(yml).not.toMatch(/disabled:/);
191
+
192
+ const { brunoConfig: reparsed } = parseCollection(yml);
193
+ expect(reparsed.clientCertificates.certs[0]).not.toHaveProperty('disabled');
194
+ });
195
+ });
196
+
197
+ describe('stringifyCollection — writing the collection version', () => {
198
+ const baseRoot = {
199
+ meta: null,
200
+ request: { headers: [], auth: { mode: 'none' }, script: { req: null, res: null }, tests: null, vars: { req: [], res: [] } },
201
+ docs: null
202
+ } as any;
203
+
204
+ it('writes the version as-is and reads back the same value', () => {
205
+ const yml = stringifyCollection(baseRoot, { name: 'c', version: '2' });
206
+ expect(yml).toMatch(/version:\s*['"]?2['"]?/);
207
+ expect(parseCollection(yml).brunoConfig.version).toBe('2');
208
+ });
209
+
210
+ it('writes no version when the collection has none', () => {
211
+ const yml = stringifyCollection(baseRoot, { name: 'c' });
212
+ expect(parseCollection(yml).brunoConfig.version).toBeUndefined();
213
+ });
214
+
215
+ it('accepts any version text — there is no check on the format', () => {
216
+ // The version is free-form: users can set whatever they like. Each of these should be
217
+ // saved and read back exactly the same, without changing the format or rejecting it.
218
+ ['v1.0.0', '2', '1.2.3-beta', '2024.01.15', 'release-2024-Q3', 'anything-goes'].forEach((version) => {
219
+ const yml = stringifyCollection(baseRoot, { name: 'c', version });
220
+ expect(parseCollection(yml).brunoConfig.version).toBe(version);
221
+ });
222
+ });
223
+ });
@@ -61,7 +61,8 @@ const hasRequestScripts = (collectionRoot: any): boolean => {
61
61
 
62
62
  const hasPresets = (brunoConfig: any): boolean => {
63
63
  return brunoConfig?.presets?.requestType?.length
64
- || brunoConfig?.presets?.requestUrl?.length;
64
+ || brunoConfig?.presets?.requestUrl?.length
65
+ || brunoConfig?.presets?.defaultEnvironment?.length;
65
66
  };
66
67
 
67
68
  const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
@@ -73,6 +74,10 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
73
74
  name: brunoConfig.name || 'Untitled Collection'
74
75
  };
75
76
 
77
+ if (brunoConfig.version != null && brunoConfig.version !== '') {
78
+ oc.info.version = String(brunoConfig.version);
79
+ }
80
+
76
81
  // collection config
77
82
  if (hasCollectionConfig(brunoConfig)) {
78
83
  oc.config = {};
@@ -144,7 +149,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
144
149
  type: 'pem',
145
150
  certificateFilePath: cert.certFilePath,
146
151
  privateKeyFilePath: cert.keyFilePath,
147
- ...(cert.passphrase && { passphrase: cert.passphrase })
152
+ ...(cert.passphrase && { passphrase: cert.passphrase }),
153
+ ...(cert.disabled === true && { disabled: true })
148
154
  };
149
155
  return pemCert;
150
156
  } else if (cert.type === 'pfx') {
@@ -152,7 +158,8 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
152
158
  domain: cert.domain,
153
159
  type: 'pkcs12',
154
160
  pkcs12FilePath: cert.pfxFilePath,
155
- ...(cert.passphrase && { passphrase: cert.passphrase })
161
+ ...(cert.passphrase && { passphrase: cert.passphrase }),
162
+ ...(cert.disabled === true && { disabled: true })
156
163
  };
157
164
  return pkcs12Cert;
158
165
  } else {
@@ -237,6 +244,7 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
237
244
  }
238
245
 
239
246
  if (hasPresets(brunoConfig)) {
247
+ const presetsExtension: any = {};
240
248
  const presetsRequest: any = {};
241
249
  if (brunoConfig.presets.requestType?.length) {
242
250
  presetsRequest.type = brunoConfig.presets.requestType;
@@ -244,22 +252,31 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
244
252
  if (brunoConfig.presets.requestUrl?.length) {
245
253
  presetsRequest.url = brunoConfig.presets.requestUrl;
246
254
  }
247
- brunoExtension.presets = {
248
- request: presetsRequest
249
- };
255
+ if (Object.keys(presetsRequest).length) {
256
+ presetsExtension.request = presetsRequest;
257
+ }
258
+ if (brunoConfig.presets.defaultEnvironment?.length) {
259
+ presetsExtension.defaultEnvironment = brunoConfig.presets.defaultEnvironment;
260
+ }
261
+ brunoExtension.presets = presetsExtension;
250
262
  }
251
263
 
252
264
  oc.extensions.bruno = brunoExtension;
253
265
  }
254
266
 
255
267
  // bruno-specific script extensions
268
+ const brunoScripts: Record<string, unknown> = {};
256
269
  if (brunoConfig.scripts?.additionalContextRoots?.length) {
270
+ brunoScripts.additionalContextRoots = brunoConfig.scripts.additionalContextRoots;
271
+ }
272
+ if (brunoConfig.scripts?.flow === 'sandwich' || brunoConfig.scripts?.flow === 'sequential') {
273
+ brunoScripts.flow = brunoConfig.scripts.flow;
274
+ }
275
+ if (Object.keys(brunoScripts).length > 0) {
257
276
  if (!oc.extensions.bruno) {
258
277
  oc.extensions.bruno = {};
259
278
  }
260
- (oc.extensions.bruno as any).scripts = {
261
- additionalContextRoots: brunoConfig.scripts.additionalContextRoots
262
- };
279
+ (oc.extensions.bruno as any).scripts = brunoScripts;
263
280
  }
264
281
 
265
282
  // bruno-specific extensions
@@ -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,70 @@
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
+ });
48
+
49
+ describe('stringifyFolder — seq', () => {
50
+ it('omits seq when the folder has none', () => {
51
+ const folderRoot = { meta: { name: 'no-seq-folder' }, docs: null } as any;
52
+
53
+ const yml = stringifyFolder(folderRoot);
54
+ const { meta } = parseFolder(yml);
55
+
56
+ expect(yml).not.toMatch(/seq:/);
57
+ expect(meta).toEqual(expect.objectContaining({ name: 'no-seq-folder' }));
58
+ expect(meta!.seq).toBeUndefined();
59
+ });
60
+
61
+ it('preserves an explicit numeric seq', () => {
62
+ const folderRoot = { meta: { name: 'ordered-folder', seq: 3 }, docs: null } as any;
63
+
64
+ const yml = stringifyFolder(folderRoot);
65
+ const { meta } = parseFolder(yml);
66
+
67
+ expect(meta).toEqual(expect.objectContaining({ name: 'ordered-folder' }));
68
+ expect(meta!.seq).toBe(3);
69
+ });
70
+ });
@@ -40,9 +40,14 @@ const stringifyFolder = (folderRoot: FolderRoot): string => {
40
40
  // info block
41
41
  const info: FolderInfo = {
42
42
  name: folderRoot.meta?.name || 'Untitled Folder',
43
- type: 'folder',
44
- seq: folderRoot.meta?.seq || 1
43
+ type: 'folder'
45
44
  };
45
+ // Only write seq when the folder actually has a numeric one. Defaulting to 1 would
46
+ // force every seq-less folder into position 1 on disk and break alphabetical fallback.
47
+ const seq = folderRoot.meta?.seq;
48
+ if (typeof seq === 'number' && Number.isFinite(seq)) {
49
+ info.seq = seq;
50
+ }
46
51
  ocFolder.info = info;
47
52
 
48
53
  // request defaults
@@ -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
+ });