@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.
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/workers/worker-script.js +1 -1
- package/dist/cjs/workers/worker-script.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/workers/worker-script.js +1 -1
- package/dist/esm/workers/worker-script.js.map +1 -1
- package/dist/formats/bru/index.d.ts.map +1 -1
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts +11 -0
- package/dist/formats/bru/utils/redact-large-text-blocks.d.ts.map +1 -0
- package/dist/formats/yml/common/app.d.ts +8 -0
- package/dist/formats/yml/common/app.d.ts.map +1 -0
- package/dist/formats/yml/common/auth.d.ts.map +1 -1
- package/dist/formats/yml/common/body.d.ts.map +1 -1
- package/dist/formats/yml/common/datatype.d.ts +14 -0
- package/dist/formats/yml/common/datatype.d.ts.map +1 -0
- package/dist/formats/yml/common/headers.d.ts.map +1 -1
- package/dist/formats/yml/common/variables.d.ts +1 -2
- package/dist/formats/yml/common/variables.d.ts.map +1 -1
- package/dist/formats/yml/index.d.ts +3 -1
- package/dist/formats/yml/index.d.ts.map +1 -1
- package/dist/formats/yml/items/parseApp.d.ts +13 -0
- package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
- package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
- package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
- package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
- package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
- package/dist/formats/yml/mockServerTypes.d.ts +95 -0
- package/dist/formats/yml/mockServerTypes.d.ts.map +1 -0
- package/dist/formats/yml/parseCollection.d.ts.map +1 -1
- package/dist/formats/yml/parseEnvironment.d.ts +3 -1
- package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
- package/dist/formats/yml/parseFolder.d.ts.map +1 -1
- package/dist/formats/yml/parseItem.d.ts.map +1 -1
- package/dist/formats/yml/parseMockServer.d.ts +4 -0
- package/dist/formats/yml/parseMockServer.d.ts.map +1 -0
- package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
- package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
- package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
- package/dist/formats/yml/stringifyFolder.d.ts.map +1 -1
- package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
- package/dist/formats/yml/stringifyMockServer.d.ts +4 -0
- package/dist/formats/yml/stringifyMockServer.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
- package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
- package/dist/workers/WorkerQueue/index.d.ts +3 -2
- package/dist/workers/WorkerQueue/index.d.ts.map +1 -1
- package/dist/workers/index.d.ts +4 -0
- package/dist/workers/index.d.ts.map +1 -1
- package/package.json +7 -5
- package/src/constants.ts +13 -0
- package/src/formats/bru/index.ts +52 -0
- package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
- package/src/formats/bru/tests/redact-large-text-blocks.spec.js +472 -0
- package/src/formats/bru/utils/redact-large-text-blocks.ts +143 -0
- package/src/formats/yml/common/actions.ts +1 -1
- package/src/formats/yml/common/app.ts +27 -0
- package/src/formats/yml/common/auth.ts +53 -1
- package/src/formats/yml/common/body.spec.ts +62 -0
- package/src/formats/yml/common/body.ts +27 -9
- package/src/formats/yml/common/datatype.spec.ts +110 -0
- package/src/formats/yml/common/datatype.ts +57 -0
- package/src/formats/yml/common/headers.ts +4 -0
- package/src/formats/yml/common/variables.spec.ts +127 -0
- package/src/formats/yml/common/variables.ts +14 -22
- package/src/formats/yml/index.ts +5 -1
- package/src/formats/yml/items/parseApp.ts +37 -0
- package/src/formats/yml/items/parseGraphQLRequest.ts +14 -16
- package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
- package/src/formats/yml/items/parseHttpRequest.ts +19 -16
- package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
- package/src/formats/yml/items/stringifyApp.ts +45 -0
- package/src/formats/yml/items/stringifyGraphQLRequest.ts +7 -6
- package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
- package/src/formats/yml/items/stringifyHttpRequest.ts +14 -6
- package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
- package/src/formats/yml/mockServerTypes.ts +95 -0
- package/src/formats/yml/parseCollection.spec.ts +193 -0
- package/src/formats/yml/parseCollection.ts +24 -6
- package/src/formats/yml/parseEnvironment.spec.ts +99 -0
- package/src/formats/yml/parseEnvironment.ts +41 -19
- package/src/formats/yml/parseFolder.spec.ts +70 -0
- package/src/formats/yml/parseFolder.ts +19 -22
- package/src/formats/yml/parseItem.spec.ts +147 -0
- package/src/formats/yml/parseItem.ts +4 -0
- package/src/formats/yml/parseMockServer.spec.ts +180 -0
- package/src/formats/yml/parseMockServer.ts +113 -0
- package/src/formats/yml/stringifyCollection.spec.ts +223 -0
- package/src/formats/yml/stringifyCollection.ts +26 -9
- package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
- package/src/formats/yml/stringifyEnvironment.ts +55 -20
- package/src/formats/yml/stringifyFolder.spec.ts +70 -0
- package/src/formats/yml/stringifyFolder.ts +7 -2
- package/src/formats/yml/stringifyItem.spec.ts +53 -0
- package/src/formats/yml/stringifyItem.ts +4 -0
- package/src/formats/yml/stringifyMockServer.spec.ts +177 -0
- package/src/formats/yml/stringifyMockServer.ts +129 -0
- package/src/formats/yml/tests/defaultEnvironment.spec.js +50 -0
- package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
- package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
- package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
- package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
- package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
- package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
- package/src/formats/yml/tests/parseCollection.spec.js +63 -0
- package/src/formats/yml/tests/parseEnvironment.spec.js +24 -63
- package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
- package/src/formats/yml/tests/stringifyEnvironment.spec.ts +230 -0
- package/src/formats/yml/tests/variables.spec.js +7 -7
- package/src/formats/yml/utils.ts +1 -1
- package/src/index.ts +39 -1
- package/src/types/bruno-lang.d.ts +1 -0
- package/src/types.ts +8 -0
- package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
- package/src/utils/secretsJsonToEnvironments.ts +129 -0
- package/src/workers/WorkerQueue/index.ts +10 -2
- package/src/workers/index.ts +25 -1
- package/src/workers/worker-script.ts +45 -21
- package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
- package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
- package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
- package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
- package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
- package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
- package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
- package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
- package/dist/formats/yml/tests/parseEnvironment.spec.d.ts +0 -2
- package/dist/formats/yml/tests/parseEnvironment.spec.d.ts.map +0 -1
- package/dist/formats/yml/tests/variables.spec.d.ts +0 -2
- 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
|
+
};
|
|
@@ -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
|
-
|
|
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:
|
|
107
|
-
|
|
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):
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|