@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,143 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { outdentString } from '@usebruno/lang';
3
+
4
+ export interface RedactedBlock {
5
+ token: string;
6
+ value: string;
7
+ }
8
+
9
+ export interface RedactionResult {
10
+ skeleton: string;
11
+ blocks: RedactedBlock[];
12
+ }
13
+
14
+ const BLOCK_TAGS = [
15
+ 'body:graphql:vars',
16
+ 'body:graphql',
17
+ 'body:json',
18
+ 'body:text',
19
+ 'body:xml',
20
+ 'body:sparql',
21
+ 'script:pre-request',
22
+ 'script:post-response',
23
+ 'tests',
24
+ 'docs',
25
+ 'body'
26
+ ];
27
+
28
+ const BLOCK_OPENING = new RegExp(`^(${BLOCK_TAGS.join('|')})[ \\t]*\\{\\r?$`);
29
+
30
+ const isOpening = (line: string): boolean => BLOCK_OPENING.test(line);
31
+
32
+ // A top-level block's closing brace sits at column 0; braces inside the content are indented, so
33
+ // nested braces are correctly treated as content rather than ending the block early.
34
+ const isClosing = (line: string): boolean => line.startsWith('}');
35
+
36
+ const tokenFor = (content: string): string => {
37
+ const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
38
+ return `__BRU_REDACTED_TEXT_BLOCK_${hash}__`;
39
+ };
40
+
41
+ const blockValue = (content: string[]): string =>
42
+ outdentString(content.join('\n').replace(/^(?:\r?\n)+/, '').replace(/\r$/, ''));
43
+
44
+ const APP_BLOCK_OPENING = /^app[ \t]*\{\r?$/;
45
+ const MULTILINE_DELIMITER = '\'\'\'';
46
+ const APP_CODE_PAIR = `code: ${MULTILINE_DELIMITER}`;
47
+
48
+ const appCodeValue = (code: string[]): string =>
49
+ code.map((line) => line.slice(4)).join('\n').trim();
50
+
51
+ const redactAppCode = (source: string, blocks: RedactedBlock[]): string => {
52
+ const lines = source.split('\n');
53
+
54
+ const start = lines.findIndex((line) => APP_BLOCK_OPENING.test(line));
55
+ if (start === -1) {
56
+ return source;
57
+ }
58
+
59
+ const closingBrace = lines.findIndex((line, index) => index > start && isClosing(line));
60
+ const end = closingBrace === -1 ? lines.length : closingBrace;
61
+
62
+ const opening = lines.findIndex((line, index) => index > start && index < end && line.trim() === APP_CODE_PAIR);
63
+ if (opening === -1) {
64
+ return source;
65
+ }
66
+
67
+ const closing = lines.findIndex(
68
+ (line, index) => index > opening && index < end && line.trim() === MULTILINE_DELIMITER
69
+ );
70
+ if (closing === -1) {
71
+ return source;
72
+ }
73
+
74
+ const code = lines.slice(opening + 1, closing);
75
+ const value = appCodeValue(code);
76
+ if (!value.length) {
77
+ return source;
78
+ }
79
+
80
+ const token = tokenFor(code.join('\n'));
81
+ blocks.push({ token, value });
82
+ return [...lines.slice(0, opening + 1), ` ${token}`, ...lines.slice(closing)].join('\n');
83
+ };
84
+
85
+ export const redactLargeBruTextBlocks = (content: string): RedactionResult => {
86
+ const source = content || '';
87
+ const skeleton: string[] = [];
88
+ const blocks: RedactedBlock[] = [];
89
+
90
+ let openTag: string | null = null;
91
+ let openContent: string[] = [];
92
+
93
+ for (const line of redactAppCode(source, blocks).split('\n')) {
94
+ if (openTag === null && isOpening(line)) {
95
+ openTag = line;
96
+ openContent = [];
97
+ continue;
98
+ }
99
+
100
+ if (openTag !== null && isClosing(line)) {
101
+ const token = tokenFor(openContent.join('\n'));
102
+ blocks.push({ token, value: blockValue(openContent) });
103
+ skeleton.push(openTag, ` ${token}`, line);
104
+ openTag = null;
105
+ continue;
106
+ }
107
+
108
+ (openTag === null ? skeleton : openContent).push(line);
109
+ }
110
+
111
+ if (openTag !== null) {
112
+ skeleton.push(openTag, ...openContent);
113
+ }
114
+
115
+ if (blocks.some((block) => source.includes(block.token))) {
116
+ console.warn('[bruno-filestore] Token collision detected; skipping redaction');
117
+ return { skeleton: source, blocks: [] };
118
+ }
119
+
120
+ return { skeleton: skeleton.join('\n'), blocks };
121
+ };
122
+
123
+ export const restoreRedactedBlocks = <T>(parsed: T, blocks: RedactedBlock[]): T => {
124
+ if (!blocks.length) {
125
+ return parsed;
126
+ }
127
+
128
+ const valueByToken = new Map(blocks.map((block) => [block.token, block.value]));
129
+
130
+ const walk = (node: any): any => {
131
+ if (typeof node === 'string') {
132
+ return valueByToken.get(node) ?? node;
133
+ }
134
+ if (node && typeof node === 'object') {
135
+ for (const key of Object.keys(node)) {
136
+ node[key] = walk(node[key]);
137
+ }
138
+ }
139
+ return node;
140
+ };
141
+
142
+ return walk(parsed);
143
+ };
@@ -17,7 +17,7 @@ export const toOpenCollectionActions = (resVariables: BrunoVariables | null | un
17
17
  type: 'set-variable',
18
18
  phase: 'after-response',
19
19
  selector: {
20
- expression: v.value || '',
20
+ expression: ensureString(v.value),
21
21
  method: 'jsonq'
22
22
  },
23
23
  variable: {
@@ -0,0 +1,27 @@
1
+ import type { App as BrunoApp } from '@usebruno/schema-types/collection/item';
2
+
3
+ export interface OpenCollectionApp {
4
+ enabled?: boolean;
5
+ code?: string;
6
+ }
7
+
8
+ export const toOpenCollectionApp = (app: BrunoApp | null | undefined): OpenCollectionApp | undefined => {
9
+ // The app block persists when enabled even before any code is written.
10
+ if (!app || (!app.code && app.enabled !== true)) return undefined;
11
+ const ocApp: OpenCollectionApp = {};
12
+ if (app.enabled === true) {
13
+ ocApp.enabled = true;
14
+ }
15
+ if (app.code) {
16
+ ocApp.code = app.code;
17
+ }
18
+ return ocApp;
19
+ };
20
+
21
+ export const toBrunoApp = (app: OpenCollectionApp | null | undefined): BrunoApp | null => {
22
+ if (!app) return null;
23
+ return {
24
+ enabled: app.enabled === true,
25
+ code: app.code || null
26
+ };
27
+ };
@@ -13,6 +13,20 @@ import type { Auth as BrunoAuth, AuthOauth1 as BrunoAuthOauth1 } from '@usebruno
13
13
  import { isString } from '../../../utils';
14
14
  import { toOpenCollectionOAuth2, toBrunoOAuth2 } from './auth-oauth2';
15
15
 
16
+ // EdgeGrid is a Bruno-specific auth mode that is not part of the OpenCollection spec,
17
+ // so its shape is defined locally and serialized using the same field names as Bruno.
18
+ interface AkamaiEdgeGridAuthValues {
19
+ type: 'akamai-edgegrid';
20
+ accessToken?: string;
21
+ clientToken?: string;
22
+ clientSecret?: string;
23
+ baseURL?: string | null;
24
+ nonce?: string | null;
25
+ timestamp?: string | null;
26
+ headersToSign?: string | null;
27
+ maxBodySize?: number | null;
28
+ }
29
+
16
30
  const buildAwsV4Auth = (config?: BrunoAuth['awsv4']): AuthAwsV4 => {
17
31
  const auth: AuthAwsV4 = { type: 'awsv4' };
18
32
 
@@ -116,6 +130,25 @@ const buildApiKeyAuth = (config?: BrunoAuth['apikey']): AuthApiKey => {
116
130
  return auth;
117
131
  };
118
132
 
133
+ const buildEdgeGridAuth = (config?: BrunoAuth['akamaiEdgegrid']): AkamaiEdgeGridAuthValues => {
134
+ const auth: AkamaiEdgeGridAuthValues = { type: 'akamai-edgegrid' };
135
+
136
+ if (!config) {
137
+ return auth;
138
+ }
139
+
140
+ if (isString(config.accessToken)) auth.accessToken = config.accessToken;
141
+ if (isString(config.clientToken)) auth.clientToken = config.clientToken;
142
+ if (isString(config.clientSecret)) auth.clientSecret = config.clientSecret;
143
+ if (isString(config.nonce)) auth.nonce = config.nonce;
144
+ if (isString(config.timestamp)) auth.timestamp = config.timestamp;
145
+ if (isString(config.baseURL)) auth.baseURL = config.baseURL;
146
+ if (isString(config.headersToSign)) auth.headersToSign = config.headersToSign;
147
+ if (typeof config.maxBodySize === 'number') auth.maxBodySize = config.maxBodySize;
148
+
149
+ return auth;
150
+ };
151
+
119
152
  const buildOAuth1Auth = (config?: BrunoAuth['oauth1']): AuthOAuth1 => {
120
153
  const auth: AuthOAuth1 = { type: 'oauth1' };
121
154
 
@@ -173,6 +206,8 @@ export const toOpenCollectionAuth = (auth?: BrunoAuth | null): Auth | undefined
173
206
  return buildOAuth1Auth(auth.oauth1);
174
207
  case 'oauth2':
175
208
  return toOpenCollectionOAuth2(auth.oauth2);
209
+ case 'akamai-edgegrid':
210
+ return buildEdgeGridAuth(auth.akamaiEdgegrid) as unknown as Auth;
176
211
  default:
177
212
  console.warn(`toOpenCollectionAuth failed: Unsupported auth mode "${auth.mode}".`);
178
213
  return undefined;
@@ -189,7 +224,8 @@ export const toBrunoAuth = (auth: Auth | null | undefined): BrunoAuth | null =>
189
224
  ntlm: null,
190
225
  oauth2: null,
191
226
  wsse: null,
192
- apikey: null
227
+ apikey: null,
228
+ akamaiEdgegrid: null
193
229
  };
194
230
 
195
231
  if (!auth) {
@@ -201,6 +237,22 @@ export const toBrunoAuth = (auth: Auth | null | undefined): BrunoAuth | null =>
201
237
  return brunoAuth;
202
238
  }
203
239
 
240
+ if ((auth as unknown as AkamaiEdgeGridAuthValues).type === 'akamai-edgegrid') {
241
+ const edgegrid = auth as unknown as AkamaiEdgeGridAuthValues;
242
+ brunoAuth.mode = 'akamai-edgegrid';
243
+ brunoAuth.akamaiEdgegrid = {
244
+ accessToken: edgegrid.accessToken || '',
245
+ clientToken: edgegrid.clientToken || '',
246
+ clientSecret: edgegrid.clientSecret || '',
247
+ nonce: edgegrid.nonce || '',
248
+ timestamp: edgegrid.timestamp || '',
249
+ baseURL: edgegrid.baseURL || '',
250
+ headersToSign: edgegrid.headersToSign || '',
251
+ maxBodySize: typeof edgegrid.maxBodySize === 'number' ? edgegrid.maxBodySize : null
252
+ };
253
+ return brunoAuth;
254
+ }
255
+
204
256
  switch (auth.type) {
205
257
  case 'awsv4':
206
258
  brunoAuth.mode = 'awsv4';
@@ -0,0 +1,62 @@
1
+ import { toOpenCollectionBody, toBrunoBody } from './body';
2
+
3
+ describe('file body description', () => {
4
+ it('toOpenCollectionBody: preserves description, omits when absent or whitespace-only', () => {
5
+ const out = toOpenCollectionBody({
6
+ mode: 'file',
7
+ file: [
8
+ { uid: 'f1', filePath: '/tmp/readme.pdf', contentType: 'application/pdf', selected: true, description: 'Upload doc' },
9
+ { uid: 'f2', filePath: '/tmp/other.bin', contentType: 'application/octet-stream', selected: false },
10
+ { uid: 'f3', filePath: '/tmp/plain.bin', contentType: 'application/octet-stream', selected: false, description: ' ' }
11
+ ]
12
+ } as any);
13
+
14
+ expect(out?.type).toBe('file');
15
+ expect(out?.data).toHaveLength(3);
16
+ expect(out?.data[0]).toMatchObject({ filePath: '/tmp/readme.pdf', description: 'Upload doc' });
17
+ expect(out?.data[1]).not.toHaveProperty('description');
18
+ expect(out?.data[2]).not.toHaveProperty('description');
19
+ });
20
+
21
+ it('toBrunoBody: parses description from file body entries', () => {
22
+ const out = toBrunoBody({
23
+ type: 'file',
24
+ data: [
25
+ { filePath: '/tmp/readme.pdf', contentType: 'application/pdf', selected: true, description: 'Upload doc' },
26
+ { filePath: '/tmp/other.bin', contentType: 'application/octet-stream', selected: false }
27
+ ]
28
+ } as any);
29
+
30
+ expect(out?.mode).toBe('file');
31
+ expect(out?.file).toHaveLength(2);
32
+
33
+ // already fails on the length check above if no bodies are recieved, the if below is just
34
+ // to satisfy typescript and then validate the content of the files
35
+ if (out?.file) {
36
+ expect(out?.file[0]).toMatchObject({ filePath: '/tmp/readme.pdf', description: 'Upload doc' });
37
+ expect(out?.file[1].description).toBeFalsy();
38
+ }
39
+ });
40
+
41
+ it('round-trips file body descriptions through OC conversion', () => {
42
+ const brunoBody = {
43
+ mode: 'file',
44
+ file: [
45
+ { uid: 'f1', filePath: '/tmp/readme.pdf', contentType: 'application/pdf', selected: true, description: 'Upload doc' },
46
+ { uid: 'f2', filePath: '/tmp/other.bin', contentType: 'application/octet-stream', selected: false }
47
+ ]
48
+ } as any;
49
+
50
+ const oc = toOpenCollectionBody(brunoBody);
51
+ const back = toBrunoBody(oc);
52
+
53
+ expect(back?.file).toHaveLength(2);
54
+
55
+ // already fails on the length check above if no bodies are recieved, the if below is just
56
+ // to satisfy typescript and then validate the content of the files
57
+ if (back?.file) {
58
+ expect(back?.file[0]).toMatchObject({ filePath: '/tmp/readme.pdf', description: 'Upload doc' });
59
+ expect(back?.file[1].description).toBeFalsy();
60
+ }
61
+ });
62
+ });
@@ -7,8 +7,10 @@ import type {
7
7
  MultipartFormBody,
8
8
  MultipartFormEntry,
9
9
  FileBody,
10
- FileBodyEntry
10
+ FileBodyVariant
11
11
  } from '@opencollection/types/requests/http';
12
+ import type { FileEntry as BrunoFileEntry } from '@usebruno/schema-types/common/file';
13
+
12
14
  import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
13
15
  import { uuid, ensureString } from '../../../utils';
14
16
 
@@ -103,12 +105,18 @@ export const toOpenCollectionBody = (body: BrunoHttpRequestBody | null | undefin
103
105
  return multipartBody;
104
106
 
105
107
  case 'file':
106
- const fileEntries: FileBodyEntry[] = body.file?.map((file): FileBodyEntry => {
107
- return {
108
+ const fileEntries: FileBodyVariant[] = body.file?.map((file): FileBodyVariant => {
109
+ const fileEntry: FileBodyVariant = {
108
110
  filePath: file.filePath || '',
109
111
  contentType: file.contentType || '',
110
112
  selected: file.selected ?? false
111
113
  };
114
+
115
+ if (file?.description?.trim().length) {
116
+ fileEntry.description = file.description;
117
+ }
118
+
119
+ return fileEntry;
112
120
  }) || [];
113
121
 
114
122
  const fileBody: FileBody = {
@@ -222,12 +230,22 @@ export const toBrunoBody = (body: HttpRequestBody | null | undefined): BrunoHttp
222
230
 
223
231
  case 'file':
224
232
  brunoBody.mode = 'file';
225
- brunoBody.file = body.data?.map((file): any => ({
226
- uid: uuid(),
227
- filePath: file.filePath || '',
228
- contentType: file.contentType || '',
229
- selected: file.selected ?? false
230
- })) || [];
233
+ brunoBody.file = body.data?.map((file): BrunoFileEntry => {
234
+ const fileEntry: BrunoFileEntry = {
235
+ uid: uuid(),
236
+ filePath: file.filePath || '',
237
+ contentType: file.contentType || '',
238
+ selected: file.selected ?? false
239
+ };
240
+
241
+ const fileWithDescription = file as FileBodyVariant;
242
+ const desc = typeof fileWithDescription.description === 'string'
243
+ ? fileWithDescription.description
244
+ : (fileWithDescription.description as { content?: string } | undefined)?.content;
245
+ if (desc && desc.trim().length) fileEntry.description = desc;
246
+
247
+ return fileEntry;
248
+ }) || [];
231
249
  break;
232
250
 
233
251
  default:
@@ -0,0 +1,110 @@
1
+ import {
2
+ isBrunoVariableDataType,
3
+ isTypedValue,
4
+ hasTypedMetadata,
5
+ toOpenCollectionTypedValue,
6
+ fromOpenCollectionTypedValue
7
+ } from './datatype';
8
+
9
+ describe('dataType helpers', () => {
10
+ describe('isBrunoVariableDataType', () => {
11
+ it('returns true for supported Bruno datatypes', () => {
12
+ expect(isBrunoVariableDataType('string')).toBe(true);
13
+ expect(isBrunoVariableDataType('number')).toBe(true);
14
+ expect(isBrunoVariableDataType('boolean')).toBe(true);
15
+ expect(isBrunoVariableDataType('object')).toBe(true);
16
+ });
17
+
18
+ it('returns false for unsupported OpenCollection datatypes (e.g. null)', () => {
19
+ expect(isBrunoVariableDataType('null')).toBe(false);
20
+ });
21
+
22
+ it('returns false for non-string inputs', () => {
23
+ expect(isBrunoVariableDataType(undefined)).toBe(false);
24
+ expect(isBrunoVariableDataType(null)).toBe(false);
25
+ expect(isBrunoVariableDataType(42)).toBe(false);
26
+ expect(isBrunoVariableDataType({})).toBe(false);
27
+ });
28
+ });
29
+
30
+ describe('isTypedValue', () => {
31
+ it('returns true for OpenCollection typed-value shapes', () => {
32
+ expect(isTypedValue({ type: 'number', data: '42' })).toBe(true);
33
+ expect(isTypedValue({ type: 'object', data: '{}' })).toBe(true);
34
+ });
35
+
36
+ it('returns false for plain strings', () => {
37
+ expect(isTypedValue('hello')).toBe(false);
38
+ expect(isTypedValue('')).toBe(false);
39
+ });
40
+
41
+ it('returns false for arrays, null, and objects missing type/data', () => {
42
+ expect(isTypedValue(null)).toBe(false);
43
+ expect(isTypedValue([])).toBe(false);
44
+ expect(isTypedValue([{ type: 'number', data: '1' }])).toBe(false);
45
+ expect(isTypedValue({ type: 'number' })).toBe(false);
46
+ expect(isTypedValue({ data: '1' })).toBe(false);
47
+ expect(isTypedValue({})).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('hasTypedMetadata', () => {
52
+ it('returns false when dataType is missing', () => {
53
+ expect(hasTypedMetadata({})).toBe(false);
54
+ });
55
+
56
+ it('returns false when dataType is the string default', () => {
57
+ expect(hasTypedMetadata({ dataType: 'string' })).toBe(false);
58
+ });
59
+
60
+ it('returns true for non-string datatypes', () => {
61
+ expect(hasTypedMetadata({ dataType: 'number' })).toBe(true);
62
+ expect(hasTypedMetadata({ dataType: 'boolean' })).toBe(true);
63
+ expect(hasTypedMetadata({ dataType: 'object' })).toBe(true);
64
+ });
65
+ });
66
+
67
+ describe('toOpenCollectionTypedValue', () => {
68
+ it('builds a {type, data} struct from dataType + stringified value', () => {
69
+ expect(toOpenCollectionTypedValue({ dataType: 'number' }, '42'))
70
+ .toEqual({ type: 'number', data: '42' });
71
+ expect(toOpenCollectionTypedValue({ dataType: 'boolean' }, 'true'))
72
+ .toEqual({ type: 'boolean', data: 'true' });
73
+ expect(toOpenCollectionTypedValue({ dataType: 'object' }, '{"a":1}'))
74
+ .toEqual({ type: 'object', data: '{"a":1}' });
75
+ });
76
+
77
+ it('falls back to type "string" when dataType is missing', () => {
78
+ expect(toOpenCollectionTypedValue({}, 'hello'))
79
+ .toEqual({ type: 'string', data: 'hello' });
80
+ });
81
+ });
82
+
83
+ describe('fromOpenCollectionTypedValue', () => {
84
+ it('coerces typed values to their declared dataType', () => {
85
+ expect(fromOpenCollectionTypedValue({ type: 'number', data: '42' }))
86
+ .toEqual({ value: 42, dataType: 'number' });
87
+ expect(fromOpenCollectionTypedValue({ type: 'boolean', data: 'true' }))
88
+ .toEqual({ value: true, dataType: 'boolean' });
89
+ expect(fromOpenCollectionTypedValue({ type: 'object', data: '{"a":1}' }))
90
+ .toEqual({ value: { a: 1 }, dataType: 'object' });
91
+ });
92
+
93
+ it('falls back to string when the declared type is unsupported (e.g. null)', () => {
94
+ const result = fromOpenCollectionTypedValue({ type: 'null' as any, data: '' });
95
+ expect(result.dataType).toBeUndefined();
96
+ });
97
+
98
+ it('coerces non-string data to a string before coercing by dataType', () => {
99
+ const result = fromOpenCollectionTypedValue({ type: 'number', data: 42 as any });
100
+ expect(result.value).toBe(42);
101
+ expect(result.dataType).toBe('number');
102
+ });
103
+
104
+ it('returns the raw string when coercion fails (e.g. non-numeric @number)', () => {
105
+ const result = fromOpenCollectionTypedValue({ type: 'number', data: 'not-a-number' });
106
+ expect(result.value).toBe('not-a-number');
107
+ expect(result.dataType).toBe('number');
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,57 @@
1
+ import type { VariableTypedValue } from '@opencollection/types/common/variables';
2
+ import {
3
+ parseValueByDataType,
4
+ BrunoVariableDataType,
5
+ isBrunoVariableDataType,
6
+ valueToString
7
+ } from '@usebruno/common/utils';
8
+
9
+ export { BrunoVariableDataType, isBrunoVariableDataType };
10
+
11
+ export const serializeVariableValue = (value: unknown): string => {
12
+ if (value !== null && typeof value === 'object') {
13
+ return JSON.stringify(value, null, 2);
14
+ }
15
+ return valueToString(value);
16
+ };
17
+
18
+ export const isTypedValue = (value: unknown): value is VariableTypedValue => {
19
+ return typeof value === 'object'
20
+ && value !== null
21
+ && !Array.isArray(value)
22
+ && 'type' in value
23
+ && 'data' in value;
24
+ };
25
+
26
+ export interface TypedVariableFields {
27
+ dataType?: BrunoVariableDataType;
28
+ }
29
+
30
+ export const hasTypedMetadata = (v: TypedVariableFields): boolean => {
31
+ return !!v.dataType && v.dataType !== 'string';
32
+ };
33
+
34
+ export const toOpenCollectionTypedValue = (
35
+ v: TypedVariableFields,
36
+ dataStr: string
37
+ ): VariableTypedValue => {
38
+ return {
39
+ type: (v.dataType || 'string') as VariableTypedValue['type'],
40
+ data: dataStr
41
+ };
42
+ };
43
+
44
+ // `'string'` is the implicit default — omitted from the result.
45
+ export const fromOpenCollectionTypedValue = (
46
+ typed: VariableTypedValue
47
+ ): { value: any } & TypedVariableFields => {
48
+ const dataStr = typeof typed.data === 'string' ? typed.data : String(typed.data ?? '');
49
+ const dataType: BrunoVariableDataType = isBrunoVariableDataType(typed.type) ? typed.type : 'string';
50
+ const result: { value: any } & TypedVariableFields = {
51
+ value: parseValueByDataType(dataStr, dataType)
52
+ };
53
+ if (dataType !== 'string') {
54
+ result.dataType = dataType;
55
+ }
56
+ return result;
57
+ };
@@ -51,6 +51,10 @@ export const toBrunoHttpHeaders = (headers: HttpRequestHeader[] | HttpResponseHe
51
51
  enabled: ('disabled' in header) ? header.disabled !== true : true
52
52
  };
53
53
 
54
+ if ('description' in header && header.description) {
55
+ brunoHeader.description = typeof header.description === 'string' ? header.description : (header.description as any)?.content || '';
56
+ }
57
+
54
58
  return brunoHeader;
55
59
  });
56
60