@usebruno/filestore 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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/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/stringifyItem.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +6 -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/package.json +7 -5
- package/src/constants.ts +9 -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 +313 -0
- package/src/formats/bru/utils/redact-large-text-blocks.ts +102 -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/items/parseApp.ts +37 -0
- package/src/formats/yml/items/parseGraphQLRequest.ts +8 -0
- package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
- package/src/formats/yml/items/parseHttpRequest.ts +13 -0
- 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 +3 -0
- package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
- package/src/formats/yml/items/stringifyHttpRequest.ts +10 -0
- package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
- package/src/formats/yml/parseCollection.spec.ts +70 -0
- package/src/formats/yml/parseCollection.ts +8 -3
- package/src/formats/yml/parseEnvironment.spec.ts +99 -0
- package/src/formats/yml/parseEnvironment.ts +41 -19
- package/src/formats/yml/parseFolder.spec.ts +52 -0
- package/src/formats/yml/parseFolder.ts +14 -21
- package/src/formats/yml/parseItem.spec.ts +147 -0
- package/src/formats/yml/parseItem.ts +4 -0
- package/src/formats/yml/stringifyCollection.spec.ts +77 -0
- package/src/formats/yml/stringifyCollection.ts +4 -0
- package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
- package/src/formats/yml/stringifyEnvironment.ts +55 -20
- package/src/formats/yml/stringifyFolder.spec.ts +47 -0
- package/src/formats/yml/stringifyItem.spec.ts +53 -0
- package/src/formats/yml/stringifyItem.ts +4 -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 +204 -0
- package/src/formats/yml/tests/variables.spec.js +7 -7
- package/src/index.ts +5 -0
- package/src/types/bruno-lang.d.ts +1 -0
- package/src/types.ts +7 -0
- package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
- package/src/utils/secretsJsonToEnvironments.ts +129 -0
- 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,147 @@
|
|
|
1
|
+
import parseItem from './parseItem';
|
|
2
|
+
|
|
3
|
+
// Typed `runtime.variables` propagate through parseItem with their dataType
|
|
4
|
+
// + coerced value. Helper-level coverage lives in the variables/datatype specs.
|
|
5
|
+
|
|
6
|
+
describe('parseItem — typed runtime.variables', () => {
|
|
7
|
+
it('coerces typed values, preserves raw value on un-coercible input, and treats explicit string as the implicit default', () => {
|
|
8
|
+
const yml = `info:
|
|
9
|
+
name: r
|
|
10
|
+
type: http
|
|
11
|
+
|
|
12
|
+
http:
|
|
13
|
+
url: https://example.com
|
|
14
|
+
method: GET
|
|
15
|
+
|
|
16
|
+
runtime:
|
|
17
|
+
variables:
|
|
18
|
+
- name: count
|
|
19
|
+
value:
|
|
20
|
+
type: number
|
|
21
|
+
data: "42"
|
|
22
|
+
- name: enabled
|
|
23
|
+
value:
|
|
24
|
+
type: boolean
|
|
25
|
+
data: "true"
|
|
26
|
+
- name: config
|
|
27
|
+
value:
|
|
28
|
+
type: object
|
|
29
|
+
data: '{"a":1}'
|
|
30
|
+
- name: greeting
|
|
31
|
+
value:
|
|
32
|
+
type: string
|
|
33
|
+
data: hi
|
|
34
|
+
- name: plain
|
|
35
|
+
value: hello
|
|
36
|
+
- name: bad
|
|
37
|
+
value:
|
|
38
|
+
type: number
|
|
39
|
+
data: 'not-a-number'
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const item = parseItem(yml);
|
|
43
|
+
const reqVars = item.request!.vars!.req!;
|
|
44
|
+
|
|
45
|
+
expect(reqVars).toHaveLength(6);
|
|
46
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
47
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
48
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
49
|
+
// Explicit `type: string` is the implicit default — no dataType materialized.
|
|
50
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
51
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
52
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
53
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
54
|
+
// Un-coercible: raw value preserved for the UI mismatch warning.
|
|
55
|
+
expect(reqVars[5]).toMatchObject({ name: 'bad', value: 'not-a-number', dataType: 'number' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('normalizes raw YAML scalars in `data` to a string before coercing', () => {
|
|
59
|
+
// YAML parses unquoted scalars per their natural JS type. The `String(... ?? '')`
|
|
60
|
+
// cast in fromOpenCollectionTypedValue keeps coercion consistent.
|
|
61
|
+
const yml = `info:
|
|
62
|
+
name: r
|
|
63
|
+
type: http
|
|
64
|
+
|
|
65
|
+
http:
|
|
66
|
+
url: https://example.com
|
|
67
|
+
method: GET
|
|
68
|
+
|
|
69
|
+
runtime:
|
|
70
|
+
variables:
|
|
71
|
+
- name: stringy
|
|
72
|
+
value:
|
|
73
|
+
type: string
|
|
74
|
+
data: 42
|
|
75
|
+
- name: numeric
|
|
76
|
+
value:
|
|
77
|
+
type: number
|
|
78
|
+
data: 42
|
|
79
|
+
- name: nullish
|
|
80
|
+
value:
|
|
81
|
+
type: number
|
|
82
|
+
data: null
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const item = parseItem(yml);
|
|
86
|
+
const reqVars = item.request!.vars!.req!;
|
|
87
|
+
|
|
88
|
+
// type=string: raw YAML number → string; no dataType field.
|
|
89
|
+
expect(reqVars[0]).toMatchObject({ name: 'stringy', value: '42' });
|
|
90
|
+
expect(reqVars[0].dataType).toBeUndefined();
|
|
91
|
+
// type=number: '42' → 42.
|
|
92
|
+
expect(reqVars[1]).toMatchObject({ name: 'numeric', value: 42, dataType: 'number' });
|
|
93
|
+
// data: null → '' (the `?? ''` arm); coerce bails on empty → raw ''.
|
|
94
|
+
expect(reqVars[2]).toMatchObject({ name: 'nullish', value: '', dataType: 'number' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('propagates typed variables for graphql/grpc/websocket requests too', () => {
|
|
98
|
+
const graphqlYml = `info:
|
|
99
|
+
name: g
|
|
100
|
+
type: graphql
|
|
101
|
+
|
|
102
|
+
graphql:
|
|
103
|
+
url: https://example.com/graphql
|
|
104
|
+
method: POST
|
|
105
|
+
|
|
106
|
+
runtime:
|
|
107
|
+
variables:
|
|
108
|
+
- name: count
|
|
109
|
+
value: { type: number, data: '7' }
|
|
110
|
+
`;
|
|
111
|
+
expect(parseItem(graphqlYml).request!.vars!.req![0]).toMatchObject({
|
|
112
|
+
name: 'count', value: 7, dataType: 'number'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const grpcYml = `info:
|
|
116
|
+
name: gr
|
|
117
|
+
type: grpc
|
|
118
|
+
|
|
119
|
+
grpc:
|
|
120
|
+
url: localhost:50051
|
|
121
|
+
|
|
122
|
+
runtime:
|
|
123
|
+
variables:
|
|
124
|
+
- name: flag
|
|
125
|
+
value: { type: boolean, data: 'false' }
|
|
126
|
+
`;
|
|
127
|
+
expect(parseItem(grpcYml).request!.vars!.req![0]).toMatchObject({
|
|
128
|
+
name: 'flag', value: false, dataType: 'boolean'
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const wsYml = `info:
|
|
132
|
+
name: ws
|
|
133
|
+
type: websocket
|
|
134
|
+
|
|
135
|
+
websocket:
|
|
136
|
+
url: wss://example.com/socket
|
|
137
|
+
|
|
138
|
+
runtime:
|
|
139
|
+
variables:
|
|
140
|
+
- name: payload
|
|
141
|
+
value: { type: object, data: '{"k":1}' }
|
|
142
|
+
`;
|
|
143
|
+
expect(parseItem(wsYml).request!.vars!.req![0]).toMatchObject({
|
|
144
|
+
name: 'payload', value: { k: 1 }, dataType: 'object'
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -10,6 +10,7 @@ import parseGraphQLRequest from './items/parseGraphQLRequest';
|
|
|
10
10
|
import parseGrpcRequest from './items/parseGrpcRequest';
|
|
11
11
|
import parseWebsocketRequest from './items/parseWebsocketRequest';
|
|
12
12
|
import parseScript from './items/parseScript';
|
|
13
|
+
import parseApp, { type AppFile } from './items/parseApp';
|
|
13
14
|
|
|
14
15
|
// Helper to get the type from an item (now in info block)
|
|
15
16
|
const getItemType = (item: Item): string | undefined => {
|
|
@@ -87,6 +88,9 @@ const parseItem = (ymlString: string): BrunoItem => {
|
|
|
87
88
|
case 'script':
|
|
88
89
|
return parseScript(ocItem as ScriptFile);
|
|
89
90
|
|
|
91
|
+
case 'app':
|
|
92
|
+
return parseApp(ocItem as unknown as AppFile);
|
|
93
|
+
|
|
90
94
|
case 'folder':
|
|
91
95
|
throw new Error('Folder items should be handled separately using parseFolder');
|
|
92
96
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import stringifyCollection from './stringifyCollection';
|
|
2
|
+
import parseCollection from './parseCollection';
|
|
3
|
+
|
|
4
|
+
// Typed collection-root vars serialize to OC's `{ type, data }` struct.
|
|
5
|
+
// `dataType: 'string'` is the implicit default and stays a raw string.
|
|
6
|
+
|
|
7
|
+
describe('stringifyCollection — typed request.variables', () => {
|
|
8
|
+
it('round-trips typed values and omits a typed struct for the implicit string default', () => {
|
|
9
|
+
const collectionRoot = {
|
|
10
|
+
meta: null,
|
|
11
|
+
request: {
|
|
12
|
+
headers: [],
|
|
13
|
+
auth: { mode: 'none' },
|
|
14
|
+
script: { req: null, res: null },
|
|
15
|
+
tests: null,
|
|
16
|
+
vars: {
|
|
17
|
+
req: [
|
|
18
|
+
{ uid: 'v1', name: 'count', value: 42, enabled: true, dataType: 'number' },
|
|
19
|
+
{ uid: 'v2', name: 'enabled', value: true, enabled: true, dataType: 'boolean' },
|
|
20
|
+
{ uid: 'v3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' },
|
|
21
|
+
{ uid: 'v4', name: 'greeting', value: 'hi', enabled: true, dataType: 'string' },
|
|
22
|
+
{ uid: 'v5', name: 'plain', value: 'hello', enabled: true }
|
|
23
|
+
],
|
|
24
|
+
res: []
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
docs: null
|
|
28
|
+
} as any;
|
|
29
|
+
|
|
30
|
+
const brunoConfig = { name: 'c' };
|
|
31
|
+
|
|
32
|
+
const yml = stringifyCollection(collectionRoot, brunoConfig);
|
|
33
|
+
|
|
34
|
+
// `type: string` is never written out.
|
|
35
|
+
expect(yml).not.toMatch(/type:\s*string/);
|
|
36
|
+
|
|
37
|
+
const { collectionRoot: reparsed } = parseCollection(yml);
|
|
38
|
+
const reqVars = reparsed.request!.vars!.req!;
|
|
39
|
+
|
|
40
|
+
expect(reqVars).toHaveLength(5);
|
|
41
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
42
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
43
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
44
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
45
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
46
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
47
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('stringifyCollection — writing the collection version', () => {
|
|
52
|
+
const baseRoot = {
|
|
53
|
+
meta: null,
|
|
54
|
+
request: { headers: [], auth: { mode: 'none' }, script: { req: null, res: null }, tests: null, vars: { req: [], res: [] } },
|
|
55
|
+
docs: null
|
|
56
|
+
} as any;
|
|
57
|
+
|
|
58
|
+
it('writes the version as-is and reads back the same value', () => {
|
|
59
|
+
const yml = stringifyCollection(baseRoot, { name: 'c', version: '2' });
|
|
60
|
+
expect(yml).toMatch(/version:\s*['"]?2['"]?/);
|
|
61
|
+
expect(parseCollection(yml).brunoConfig.version).toBe('2');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('writes no version when the collection has none', () => {
|
|
65
|
+
const yml = stringifyCollection(baseRoot, { name: 'c' });
|
|
66
|
+
expect(parseCollection(yml).brunoConfig.version).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('accepts any version text — there is no check on the format', () => {
|
|
70
|
+
// The version is free-form: users can set whatever they like. Each of these should be
|
|
71
|
+
// saved and read back exactly the same, without changing the format or rejecting it.
|
|
72
|
+
['v1.0.0', '2', '1.2.3-beta', '2024.01.15', 'release-2024-Q3', 'anything-goes'].forEach((version) => {
|
|
73
|
+
const yml = stringifyCollection(baseRoot, { name: 'c', version });
|
|
74
|
+
expect(parseCollection(yml).brunoConfig.version).toBe(version);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -73,6 +73,10 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
|
|
|
73
73
|
name: brunoConfig.name || 'Untitled Collection'
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
if (brunoConfig.version != null && brunoConfig.version !== '') {
|
|
77
|
+
oc.info.version = String(brunoConfig.version);
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
// collection config
|
|
77
81
|
if (hasCollectionConfig(brunoConfig)) {
|
|
78
82
|
oc.config = {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import stringifyEnvironment, { toOpenCollectionEnvironmentVariables } from './stringifyEnvironment';
|
|
2
|
+
import parseEnvironment from './parseEnvironment';
|
|
3
|
+
|
|
4
|
+
describe('toOpenCollectionEnvironmentVariables', () => {
|
|
5
|
+
it('returns undefined for null / empty input', () => {
|
|
6
|
+
expect(toOpenCollectionEnvironmentVariables([])).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('serializes plain string variables as raw strings', () => {
|
|
10
|
+
const out = toOpenCollectionEnvironmentVariables([
|
|
11
|
+
{ uid: 'u1', name: 'apiKey', value: 'abc', type: 'text', enabled: true, secret: false } as any
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
expect(out).toEqual([{ name: 'apiKey', value: 'abc' }]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('serializes typed variables as a {type, data} struct', () => {
|
|
18
|
+
const out = toOpenCollectionEnvironmentVariables([
|
|
19
|
+
{ uid: 'u1', name: 'port', value: 300, type: 'text', enabled: true, secret: false, dataType: 'number' } as any,
|
|
20
|
+
{ uid: 'u2', name: 'flag', value: true, type: 'text', enabled: true, secret: false, dataType: 'boolean' } as any,
|
|
21
|
+
{ uid: 'u3', name: 'config', value: { a: 1 }, type: 'text', enabled: true, secret: false, dataType: 'object' } as any
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
expect(out).toEqual([
|
|
25
|
+
{ name: 'port', value: { type: 'number', data: '300' } },
|
|
26
|
+
{ name: 'flag', value: { type: 'boolean', data: 'true' } },
|
|
27
|
+
{ name: 'config', value: { type: 'object', data: '{\n "a": 1\n}' } }
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('serializes secret variables without a value but preserves the dataType as `type`', () => {
|
|
32
|
+
const out = toOpenCollectionEnvironmentVariables([
|
|
33
|
+
{ uid: 'u1', name: 'apiKey', value: '', type: 'text', enabled: true, secret: true } as any,
|
|
34
|
+
{ uid: 'u2', name: 'flag', value: '', type: 'text', enabled: false, secret: true, dataType: 'number' } as any
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
expect(out).toEqual([
|
|
38
|
+
{ secret: true, name: 'apiKey' },
|
|
39
|
+
{ secret: true, name: 'flag', type: 'number', disabled: true }
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('marks disabled non-secret variables', () => {
|
|
44
|
+
const out = toOpenCollectionEnvironmentVariables([
|
|
45
|
+
{ uid: 'u1', name: 'apiKey', value: 'abc', type: 'text', enabled: false, secret: false } as any
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
expect(out).toEqual([{ name: 'apiKey', value: 'abc', disabled: true }]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('stringifyEnvironment', () => {
|
|
53
|
+
it('round-trips through parseEnvironment for typed and secret variables', () => {
|
|
54
|
+
const env = {
|
|
55
|
+
uid: 'env-uid',
|
|
56
|
+
name: 'test_env',
|
|
57
|
+
variables: [
|
|
58
|
+
{ uid: 'u1', name: 'env_str', value: 'hello', type: 'text', enabled: true, secret: false },
|
|
59
|
+
{ uid: 'u2', name: 'env_num', value: 300, type: 'text', enabled: true, secret: false, dataType: 'number' as const },
|
|
60
|
+
{ uid: 'u3', name: 'env_bool', value: true, type: 'text', enabled: true, secret: false, dataType: 'boolean' as const },
|
|
61
|
+
{ uid: 'u4', name: 'env_obj', value: { scope: 'env' }, type: 'text', enabled: true, secret: false, dataType: 'object' as const },
|
|
62
|
+
{ uid: 'u5', name: 'env_secret', value: '', type: 'text', enabled: true, secret: true },
|
|
63
|
+
{ uid: 'u6', name: 'env_secret_num', value: '', type: 'text', enabled: true, secret: true, dataType: 'number' as const }
|
|
64
|
+
]
|
|
65
|
+
} as any;
|
|
66
|
+
|
|
67
|
+
const yml = stringifyEnvironment(env);
|
|
68
|
+
const reparsed = parseEnvironment(yml);
|
|
69
|
+
|
|
70
|
+
expect(reparsed.name).toBe('test_env');
|
|
71
|
+
expect(reparsed.variables[0]).toMatchObject({ name: 'env_str', value: 'hello', secret: false });
|
|
72
|
+
expect(reparsed.variables[1]).toMatchObject({ name: 'env_num', value: 300, dataType: 'number', secret: false });
|
|
73
|
+
expect(reparsed.variables[2]).toMatchObject({ name: 'env_bool', value: true, dataType: 'boolean', secret: false });
|
|
74
|
+
expect(reparsed.variables[3]).toMatchObject({ name: 'env_obj', value: { scope: 'env' }, dataType: 'object', secret: false });
|
|
75
|
+
expect(reparsed.variables[4]).toMatchObject({ name: 'env_secret', secret: true, value: '' });
|
|
76
|
+
expect(reparsed.variables[4].dataType).toBeUndefined();
|
|
77
|
+
expect(reparsed.variables[5]).toMatchObject({ name: 'env_secret_num', secret: true, value: '', dataType: 'number' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('preserves the color field when present', () => {
|
|
81
|
+
const env = {
|
|
82
|
+
uid: 'env-uid',
|
|
83
|
+
name: 'colorful',
|
|
84
|
+
color: '#ff0000',
|
|
85
|
+
variables: []
|
|
86
|
+
} as any;
|
|
87
|
+
|
|
88
|
+
const reparsed = parseEnvironment(stringifyEnvironment(env));
|
|
89
|
+
expect(reparsed.color).toBe('#ff0000');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,53 +1,89 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
.
|
|
14
|
-
|
|
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
|
|
25
|
-
secretVar.type = v.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
variables: (environment.externalSecrets.variables || []).map((variable) => ({ ...variable }))
|
|
72
|
-
};
|
|
104
|
+
const ocExternalSecrets = toOpenCollectionExternalSecrets(environment.externalSecrets);
|
|
105
|
+
if (ocExternalSecrets) {
|
|
106
|
+
(ocEnvironment as any).externalSecrets = ocExternalSecrets;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
return stringifyYml(ocEnvironment);
|
|
@@ -78,4 +112,5 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => {
|
|
|
78
112
|
throw error;
|
|
79
113
|
}
|
|
80
114
|
};
|
|
115
|
+
|
|
81
116
|
export default stringifyEnvironment;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import stringifyFolder from './stringifyFolder';
|
|
2
|
+
import parseFolder from './parseFolder';
|
|
3
|
+
|
|
4
|
+
// Typed folder vars serialize to OC's `{ type, data }` struct.
|
|
5
|
+
// `dataType: 'string'` is the implicit default and stays a raw string.
|
|
6
|
+
|
|
7
|
+
describe('stringifyFolder — typed request.variables', () => {
|
|
8
|
+
it('round-trips typed values and omits a typed struct for the implicit string default', () => {
|
|
9
|
+
const folderRoot = {
|
|
10
|
+
meta: { name: 'my-folder', seq: 1 },
|
|
11
|
+
request: {
|
|
12
|
+
headers: [],
|
|
13
|
+
auth: { mode: 'none' },
|
|
14
|
+
script: { req: null, res: null },
|
|
15
|
+
tests: null,
|
|
16
|
+
vars: {
|
|
17
|
+
req: [
|
|
18
|
+
{ uid: 'v1', name: 'count', value: 42, enabled: true, dataType: 'number' },
|
|
19
|
+
{ uid: 'v2', name: 'enabled', value: true, enabled: true, dataType: 'boolean' },
|
|
20
|
+
{ uid: 'v3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' },
|
|
21
|
+
{ uid: 'v4', name: 'greeting', value: 'hi', enabled: true, dataType: 'string' },
|
|
22
|
+
{ uid: 'v5', name: 'plain', value: 'hello', enabled: true }
|
|
23
|
+
],
|
|
24
|
+
res: []
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
docs: null
|
|
28
|
+
} as any;
|
|
29
|
+
|
|
30
|
+
const yml = stringifyFolder(folderRoot);
|
|
31
|
+
|
|
32
|
+
// `type: string` is never written out.
|
|
33
|
+
expect(yml).not.toMatch(/type:\s*string/);
|
|
34
|
+
|
|
35
|
+
const reparsed = parseFolder(yml);
|
|
36
|
+
const reqVars = reparsed.request!.vars!.req!;
|
|
37
|
+
|
|
38
|
+
expect(reqVars).toHaveLength(5);
|
|
39
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
40
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
41
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
42
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
43
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
44
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
45
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import stringifyItem from './stringifyItem';
|
|
2
|
+
import parseItem from './parseItem';
|
|
3
|
+
|
|
4
|
+
// Typed bruno vars on `request.vars.req` serialize to OC's `{ type, data }`
|
|
5
|
+
// struct. `dataType: 'string'` is the implicit default and stays a raw string.
|
|
6
|
+
|
|
7
|
+
describe('stringifyItem — typed runtime.variables', () => {
|
|
8
|
+
it('round-trips typed values and omits a typed struct for the implicit string default', () => {
|
|
9
|
+
const item = {
|
|
10
|
+
uid: 'i1',
|
|
11
|
+
type: 'http-request',
|
|
12
|
+
name: 'r',
|
|
13
|
+
seq: 1,
|
|
14
|
+
request: {
|
|
15
|
+
url: 'https://example.com',
|
|
16
|
+
method: 'GET',
|
|
17
|
+
headers: [],
|
|
18
|
+
params: [],
|
|
19
|
+
body: { mode: 'none' },
|
|
20
|
+
auth: { mode: 'none' },
|
|
21
|
+
script: { req: null, res: null },
|
|
22
|
+
tests: null,
|
|
23
|
+
vars: {
|
|
24
|
+
req: [
|
|
25
|
+
{ uid: 'v1', name: 'count', value: 42, enabled: true, dataType: 'number' },
|
|
26
|
+
{ uid: 'v2', name: 'enabled', value: true, enabled: true, dataType: 'boolean' },
|
|
27
|
+
{ uid: 'v3', name: 'config', value: { a: 1 }, enabled: true, dataType: 'object' },
|
|
28
|
+
{ uid: 'v4', name: 'greeting', value: 'hi', enabled: true, dataType: 'string' },
|
|
29
|
+
{ uid: 'v5', name: 'plain', value: 'hello', enabled: true }
|
|
30
|
+
],
|
|
31
|
+
res: []
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} as any;
|
|
35
|
+
|
|
36
|
+
const yml = stringifyItem(item);
|
|
37
|
+
|
|
38
|
+
// `type: string` is never written out.
|
|
39
|
+
expect(yml).not.toMatch(/type:\s*string/);
|
|
40
|
+
|
|
41
|
+
const reparsed = parseItem(yml);
|
|
42
|
+
const reqVars = reparsed.request!.vars!.req!;
|
|
43
|
+
|
|
44
|
+
expect(reqVars).toHaveLength(5);
|
|
45
|
+
expect(reqVars[0]).toMatchObject({ name: 'count', value: 42, dataType: 'number' });
|
|
46
|
+
expect(reqVars[1]).toMatchObject({ name: 'enabled', value: true, dataType: 'boolean' });
|
|
47
|
+
expect(reqVars[2]).toMatchObject({ name: 'config', value: { a: 1 }, dataType: 'object' });
|
|
48
|
+
expect(reqVars[3]).toMatchObject({ name: 'greeting', value: 'hi' });
|
|
49
|
+
expect(reqVars[3].dataType).toBeUndefined();
|
|
50
|
+
expect(reqVars[4]).toMatchObject({ name: 'plain', value: 'hello' });
|
|
51
|
+
expect(reqVars[4].dataType).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -4,6 +4,7 @@ import stringifyGraphqlRequest from './items/stringifyGraphQLRequest';
|
|
|
4
4
|
import stringifyGrpcRequest from './items/stringifyGrpcRequest';
|
|
5
5
|
import stringifyWebsocketRequest from './items/stringifyWebsocketRequest';
|
|
6
6
|
import stringifyScript from './items/stringifyScript';
|
|
7
|
+
import stringifyApp from './items/stringifyApp';
|
|
7
8
|
|
|
8
9
|
const stringifyItem = (item: BrunoItem): string => {
|
|
9
10
|
try {
|
|
@@ -23,6 +24,9 @@ const stringifyItem = (item: BrunoItem): string => {
|
|
|
23
24
|
case 'js':
|
|
24
25
|
return stringifyScript(item);
|
|
25
26
|
|
|
27
|
+
case 'app':
|
|
28
|
+
return stringifyApp(item);
|
|
29
|
+
|
|
26
30
|
case 'folder':
|
|
27
31
|
throw new Error('Folder items should be handled separately using stringifyFolder');
|
|
28
32
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
opencollection: 1.0.0
|
|
2
|
+
|
|
3
|
+
info:
|
|
4
|
+
name: md8
|
|
5
|
+
config:
|
|
6
|
+
proxy:
|
|
7
|
+
inherit: true
|
|
8
|
+
config:
|
|
9
|
+
protocol: http
|
|
10
|
+
hostname: ""
|
|
11
|
+
port: ""
|
|
12
|
+
auth:
|
|
13
|
+
username: ""
|
|
14
|
+
password: ""
|
|
15
|
+
bypassProxy: ""
|
|
16
|
+
bundled: false
|
|
17
|
+
extensions:
|
|
18
|
+
bruno:
|
|
19
|
+
ignore:
|
|
20
|
+
- node_modules
|
|
21
|
+
- .git
|
|
22
|
+
presets:
|
|
23
|
+
request:
|
|
24
|
+
type: graphql
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
opencollection: 1.0.0
|
|
2
|
+
|
|
3
|
+
info:
|
|
4
|
+
name: md8
|
|
5
|
+
config:
|
|
6
|
+
proxy:
|
|
7
|
+
inherit: true
|
|
8
|
+
config:
|
|
9
|
+
protocol: http
|
|
10
|
+
hostname: ""
|
|
11
|
+
port: ""
|
|
12
|
+
auth:
|
|
13
|
+
username: ""
|
|
14
|
+
password: ""
|
|
15
|
+
bypassProxy: ""
|
|
16
|
+
bundled: false
|
|
17
|
+
extensions:
|
|
18
|
+
bruno:
|
|
19
|
+
ignore:
|
|
20
|
+
- node_modules
|
|
21
|
+
- .git
|
|
22
|
+
presets:
|
|
23
|
+
request:
|
|
24
|
+
type: http
|
|
25
|
+
url: https://example.com/graphql
|