@usebruno/filestore 0.9.0 → 0.10.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.
@@ -1,6 +1,7 @@
1
- import { Variable } from '@opencollection/types/common/variables';
1
+ import { Variable, VariableTypedValue } from '@opencollection/types/common/variables';
2
2
  import { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';
3
3
  import { Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';
4
+ export declare const isTypedValue: (value: unknown) => value is VariableTypedValue;
4
5
  /**
5
6
  * Convert Bruno pre-request variables to OpenCollection variables format.
6
7
  * Note: Post-response variables are now converted to actions (see actions.ts).
@@ -1 +1 @@
1
- {"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../../../../src/formats/yml/common/variables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAClE,OAAO,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAA6B,SAAS,IAAI,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAGjH;;;GAGG;AACH,eAAO,MAAM,yBAAyB,cAAe,kBAAkB,CAAC,MAAM,CAAC,GAAG,cAAc,GAAG,IAAI,GAAG,SAAS,KAAG,QAAQ,EAAE,GAAG,SA4BlI,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,cAAe,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,KAAG;IAAE,GAAG,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,cAAc,CAAA;CAwBrH,CAAC"}
1
+ {"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../../../../src/formats/yml/common/variables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAC/F,OAAO,EAA6B,SAAS,IAAI,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAGjH,eAAO,MAAM,YAAY,UAAW,OAAO,gCAQ1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,cAAe,kBAAkB,CAAC,MAAM,CAAC,GAAG,cAAc,GAAG,IAAI,GAAG,SAAS,KAAG,QAAQ,EAAE,GAAG,SA+BlI,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,cAAe,QAAQ,EAAE,GAAG,IAAI,GAAG,SAAS,KAAG;IAAE,GAAG,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,cAAc,CAAA;CAiCrH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"parseEnvironment.d.ts","sourceRoot":"","sources":["../../../src/formats/yml/parseEnvironment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,IAAI,gBAAgB,EAAmD,MAAM,+CAA+C,CAAC;AAsCtJ,QAAA,MAAM,gBAAgB,cAAe,MAAM,KAAG,gBAgB7C,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"parseEnvironment.d.ts","sourceRoot":"","sources":["../../../src/formats/yml/parseEnvironment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,IAAI,gBAAgB,EAAmD,MAAM,+CAA+C,CAAC;AA2EtJ,QAAA,MAAM,gBAAgB,cAAe,MAAM,KAAG,gBAqB7C,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"stringifyEnvironment.d.ts","sourceRoot":"","sources":["../../../src/formats/yml/stringifyEnvironment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,IAAI,gBAAgB,EAAmD,MAAM,+CAA+C,CAAC;AA2CtJ,QAAA,MAAM,oBAAoB,gBAAiB,gBAAgB,KAAG,MAsB7D,CAAC;AACF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"stringifyEnvironment.d.ts","sourceRoot":"","sources":["../../../src/formats/yml/stringifyEnvironment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,IAAI,gBAAgB,EAAmD,MAAM,+CAA+C,CAAC;AAkDtJ,QAAA,MAAM,oBAAoB,gBAAiB,gBAAgB,KAAG,MA6B7D,CAAC;AACF,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=parseEnvironment.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseEnvironment.spec.d.ts","sourceRoot":"","sources":["../../../../src/formats/yml/tests/parseEnvironment.spec.js"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=variables.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variables.spec.d.ts","sourceRoot":"","sources":["../../../../src/formats/yml/tests/variables.spec.js"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebruno/filestore",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -29,7 +29,7 @@
29
29
  "@types/jest": "^29.5.11",
30
30
  "@types/lodash": "^4.14.191",
31
31
  "@types/node": "^24.1.0",
32
- "@usebruno/schema-types": "0.4.0",
32
+ "@usebruno/schema-types": "0.6.0",
33
33
  "babel-jest": "^29.7.0",
34
34
  "jest": "^29.2.0",
35
35
  "nanoid": "3.3.8",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@types/nanoid": "^2.1.0",
48
- "@usebruno/lang": "0.36.0",
48
+ "@usebruno/lang": "0.37.0",
49
49
  "ajv": "^8.17.1",
50
50
  "lodash": "^4.17.21",
51
51
  "yaml": "^2.3.4"
@@ -1,8 +1,18 @@
1
- import { Variable } from '@opencollection/types/common/variables';
1
+ import { Variable, VariableTypedValue } from '@opencollection/types/common/variables';
2
2
  import { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';
3
3
  import { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';
4
4
  import { uuid, ensureString } from '../../../utils';
5
5
 
6
+ export const isTypedValue = (value: unknown): value is VariableTypedValue => {
7
+ return (
8
+ typeof value === 'object'
9
+ && value !== null
10
+ && !Array.isArray(value)
11
+ && 'type' in value
12
+ && 'data' in value
13
+ );
14
+ };
15
+
6
16
  /**
7
17
  * Convert Bruno pre-request variables to OpenCollection variables format.
8
18
  * Note: Post-response variables are now converted to actions (see actions.ts).
@@ -21,7 +31,10 @@ export const toOpenCollectionVariables = (variables: BrunoFolderRequest['vars']
21
31
  const ocVariables: Variable[] = reqVarsArray.map((v: BrunoVariable): Variable => {
22
32
  const variable: Variable = {
23
33
  name: v.name || '',
24
- value: v.value || ''
34
+ value:
35
+ v.datatype && v.datatype !== 'string'
36
+ ? { type: v.datatype, data: ensureString(v.value) }
37
+ : v.value || ''
25
38
  };
26
39
 
27
40
  if (v?.description?.trim().length) {
@@ -52,11 +65,20 @@ export const toBrunoVariables = (variables: Variable[] | null | undefined): { re
52
65
  const variable: BrunoVariable = {
53
66
  uid: uuid(),
54
67
  name: ensureString(v.name),
55
- value: ensureString(v.value),
68
+ value: '',
56
69
  enabled: v.disabled !== true,
57
70
  local: false
58
71
  };
59
72
 
73
+ if (isTypedValue(v.value)) {
74
+ variable.value = ensureString(v.value.data);
75
+ if (v.value.type !== 'string' && v.value.type !== 'null') {
76
+ variable.datatype = v.value.type;
77
+ }
78
+ } else {
79
+ variable.value = ensureString(v.value);
80
+ }
81
+
60
82
  if (v.description) {
61
83
  variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';
62
84
  }
@@ -3,6 +3,7 @@ import type { Environment } from '@opencollection/types/config/environments';
3
3
  import type { Variable, SecretVariable } from '@opencollection/types/common/variables';
4
4
  import { parseYml } from './utils';
5
5
  import { uuid, ensureString } from '../../utils';
6
+ import { isTypedValue } from './common/variables';
6
7
 
7
8
  const isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {
8
9
  return 'secret' in v && v.secret === true;
@@ -15,7 +16,7 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
15
16
 
16
17
  return variables.map((v): BrunoEnvironmentVariable => {
17
18
  if (isSecretVariable(v)) {
18
- return {
19
+ const variable: BrunoEnvironmentVariable = {
19
20
  uid: uuid(),
20
21
  name: ensureString(v.name),
21
22
  value: '',
@@ -23,19 +24,55 @@ const toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] |
23
24
  enabled: v.disabled !== true,
24
25
  secret: true
25
26
  };
27
+
28
+ if (v.type && v.type !== 'string' && v.type !== 'null') {
29
+ variable.datatype = v.type;
30
+ }
31
+
32
+ return variable;
26
33
  }
27
34
  const variable: BrunoEnvironmentVariable = {
28
35
  uid: uuid(),
29
36
  name: ensureString(v.name),
30
- value: ensureString(v.value),
37
+ value: '',
31
38
  type: 'text',
32
39
  enabled: v.disabled !== true,
33
40
  secret: false
34
41
  };
42
+
43
+ if (isTypedValue(v.value)) {
44
+ variable.value = ensureString(v.value.data);
45
+ if (v.value.type !== 'string' && v.value.type !== 'null') {
46
+ variable.datatype = v.value.type;
47
+ }
48
+ } else {
49
+ variable.value = ensureString(v.value);
50
+ }
51
+
35
52
  return variable;
36
53
  });
37
54
  };
38
55
 
56
+ const toBrunoExternalSecrets = (externalSecrets: any): BrunoEnvironment['externalSecrets'] | undefined => {
57
+ if (!externalSecrets || typeof externalSecrets !== 'object') {
58
+ return undefined;
59
+ }
60
+
61
+ const variables = Array.isArray(externalSecrets.variables)
62
+ ? externalSecrets.variables.map((variable: any) => {
63
+ const result: Record<string, string> = { name: ensureString(variable?.name) };
64
+ Object.keys(variable || {}).forEach((key) => {
65
+ if (key !== 'name') {
66
+ result[key] = ensureString(variable[key]);
67
+ }
68
+ });
69
+ return result;
70
+ })
71
+ : [];
72
+
73
+ return { type: ensureString(externalSecrets.type), variables } as BrunoEnvironment['externalSecrets'];
74
+ };
75
+
39
76
  const parseEnvironment = (ymlString: string): BrunoEnvironment => {
40
77
  try {
41
78
  const ocEnvironment: Environment = parseYml(ymlString);
@@ -47,6 +84,11 @@ const parseEnvironment = (ymlString: string): BrunoEnvironment => {
47
84
  color: ocEnvironment.color || null
48
85
  };
49
86
 
87
+ const externalSecrets = toBrunoExternalSecrets((ocEnvironment as any).externalSecrets);
88
+ if (externalSecrets) {
89
+ brunoEnvironment.externalSecrets = externalSecrets;
90
+ }
91
+
50
92
  return brunoEnvironment;
51
93
  } catch (error) {
52
94
  console.error('Error parsing environment:', error);
@@ -2,6 +2,7 @@ import type { Environment as BrunoEnvironment, EnvironmentVariable as BrunoEnvir
2
2
  import type { Environment } from '@opencollection/types/config/environments';
3
3
  import type { Variable, SecretVariable } from '@opencollection/types/common/variables';
4
4
  import { stringifyYml } from './utils';
5
+ import { ensureString } from '../../utils';
5
6
 
6
7
  const toOpenCollectionEnvironmentVariables = (variables: BrunoEnvironmentVariable[]): (Variable | SecretVariable)[] | undefined => {
7
8
  if (!variables?.length) {
@@ -20,6 +21,9 @@ const toOpenCollectionEnvironmentVariables = (variables: BrunoEnvironmentVariabl
20
21
  secret: true,
21
22
  name: v.name || ''
22
23
  };
24
+ if (v.datatype && v.datatype !== 'string') {
25
+ secretVar.type = v.datatype;
26
+ }
23
27
  if (v.enabled === false) {
24
28
  secretVar.disabled = true;
25
29
  }
@@ -28,7 +32,10 @@ const toOpenCollectionEnvironmentVariables = (variables: BrunoEnvironmentVariabl
28
32
 
29
33
  const variable: Variable = {
30
34
  name: v.name || '',
31
- value: v.value as string
35
+ value:
36
+ v.datatype && v.datatype !== 'string'
37
+ ? { type: v.datatype, data: ensureString(v.value) }
38
+ : ensureString(v.value)
32
39
  };
33
40
 
34
41
  if (v.enabled === false) {
@@ -58,6 +65,13 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => {
58
65
  }
59
66
  }
60
67
 
68
+ if (environment.externalSecrets) {
69
+ (ocEnvironment as any).externalSecrets = {
70
+ type: environment.externalSecrets.type,
71
+ variables: (environment.externalSecrets.variables || []).map((variable) => ({ ...variable }))
72
+ };
73
+ }
74
+
61
75
  return stringifyYml(ocEnvironment);
62
76
  } catch (error) {
63
77
  console.error('Error stringifying environment:', error);
@@ -0,0 +1,161 @@
1
+ import parseEnvironment from '../parseEnvironment';
2
+ import stringifyEnvironment from '../stringifyEnvironment';
3
+
4
+ const ENV_YML = `name: test_env
5
+ variables:
6
+ - name: env_str
7
+ value: env_string
8
+ - name: env_num
9
+ value:
10
+ type: number
11
+ data: "300"
12
+ - name: env_bool
13
+ value:
14
+ type: boolean
15
+ data: "true"
16
+ - name: env_obj
17
+ value:
18
+ type: object
19
+ data: |-
20
+ {
21
+ "scope": "env"
22
+ }
23
+ - name: falsy_num
24
+ value:
25
+ type: number
26
+ data: "0"
27
+ - name: falsy_bool
28
+ value:
29
+ type: boolean
30
+ data: "false"
31
+ - secret: true
32
+ name: env_secret_str
33
+ - secret: true
34
+ name: env_secret_num
35
+ type: number
36
+ - secret: true
37
+ name: env_secret_bool
38
+ type: boolean
39
+ - secret: true
40
+ name: env_secret_obj
41
+ type: object
42
+ - name: env_array_obj
43
+ value:
44
+ type: object
45
+ data: "[1,2,3,4]"
46
+ `;
47
+
48
+ const byName = (env) => Object.fromEntries(env.variables.map((v) => [v.name, v]));
49
+
50
+ describe('yml parseEnvironment - typed values', () => {
51
+ it('keeps the value as a string and preserves the type via datatype', () => {
52
+ const variables = byName(parseEnvironment(ENV_YML));
53
+
54
+ expect(variables.env_num).toMatchObject({ value: '300', type: 'text', datatype: 'number' });
55
+ expect(typeof variables.env_num.value).toBe('string');
56
+
57
+ expect(variables.env_bool).toMatchObject({ value: 'true', datatype: 'boolean' });
58
+ expect(variables.falsy_num).toMatchObject({ value: '0', datatype: 'number' });
59
+ expect(variables.falsy_bool).toMatchObject({ value: 'false', datatype: 'boolean' });
60
+
61
+ expect(variables.env_obj.datatype).toBe('object');
62
+ expect(typeof variables.env_obj.value).toBe('string');
63
+ expect(variables.env_obj.value).toContain('"scope": "env"');
64
+
65
+ expect(variables.env_array_obj).toMatchObject({ value: '[1,2,3,4]', datatype: 'object' });
66
+ });
67
+
68
+ it('does not set datatype for plain string values', () => {
69
+ const variables = byName(parseEnvironment(ENV_YML));
70
+
71
+ expect(variables.env_str).toMatchObject({ value: 'env_string', type: 'text', secret: false });
72
+ expect(variables.env_str).not.toHaveProperty('datatype');
73
+ });
74
+
75
+ it('parses secret variables with no value or datatype', () => {
76
+ const variables = byName(parseEnvironment(ENV_YML));
77
+
78
+ expect(variables.env_secret_str).toMatchObject({ name: 'env_secret_str', value: '', secret: true });
79
+ expect(variables.env_secret_str).not.toHaveProperty('datatype');
80
+ });
81
+
82
+ it('parses secret variables with a type, keeping the value empty and the type in datatype', () => {
83
+ const variables = byName(parseEnvironment(ENV_YML));
84
+
85
+ expect(variables.env_secret_num).toMatchObject({ value: '', secret: true, datatype: 'number' });
86
+ expect(variables.env_secret_bool).toMatchObject({ value: '', secret: true, datatype: 'boolean' });
87
+ expect(variables.env_secret_obj).toMatchObject({ value: '', secret: true, datatype: 'object' });
88
+ });
89
+
90
+ it('serializes secret variable datatype back to a type field, omitting it for plain secrets', () => {
91
+ const yml = stringifyEnvironment(parseEnvironment(ENV_YML));
92
+
93
+ expect(yml).toContain('- secret: true\n name: env_secret_num\n type: number');
94
+ expect(yml).toContain('- secret: true\n name: env_secret_bool\n type: boolean');
95
+ expect(yml).toContain('- secret: true\n name: env_secret_obj\n type: object');
96
+ expect(yml).toContain('- secret: true\n name: env_secret_str');
97
+ expect(yml).not.toContain('name: env_secret_str\n type:');
98
+ });
99
+
100
+ it('serializes datatype back to an OpenCollection { type, data } value', () => {
101
+ const yml = stringifyEnvironment(parseEnvironment(ENV_YML));
102
+
103
+ expect(yml).toContain('type: number');
104
+ expect(yml).toContain('data: "300"');
105
+ expect(yml).toContain('type: boolean');
106
+ expect(yml).toContain('type: object');
107
+ // plain strings stay plain, never wrapped as a string datatype
108
+ expect(yml).toContain('value: env_string');
109
+ expect(yml).not.toContain('type: string');
110
+ });
111
+
112
+ it('round-trips value and datatype through parse -> stringify -> parse', () => {
113
+ const env = parseEnvironment(ENV_YML);
114
+ const reparsed = parseEnvironment(stringifyEnvironment(env));
115
+
116
+ const withoutUid = (e) => e.variables.map(({ uid, ...rest }) => rest);
117
+ expect(withoutUid(reparsed)).toEqual(withoutUid(env));
118
+ });
119
+ });
120
+
121
+ const EXTERNAL_SECRETS_YML = `name: test_env
122
+ variables:
123
+ - name: env_str
124
+ value: env_string
125
+ externalSecrets:
126
+ type: my-vault
127
+ variables:
128
+ - name: by_path
129
+ path: secret/data/secret
130
+ - name: by_secret_name
131
+ secretName: secret
132
+ - name: by_vault_name
133
+ vaultName: secret
134
+ `;
135
+
136
+ describe('yml parseEnvironment - external secrets', () => {
137
+ it('parses externalSecrets, preserving the type and arbitrary variable keys', () => {
138
+ const env = parseEnvironment(EXTERNAL_SECRETS_YML);
139
+
140
+ expect(env.externalSecrets).toEqual({
141
+ type: 'my-vault',
142
+ variables: [
143
+ { name: 'by_path', path: 'secret/data/secret' },
144
+ { name: 'by_secret_name', secretName: 'secret' },
145
+ { name: 'by_vault_name', vaultName: 'secret' }
146
+ ]
147
+ });
148
+ });
149
+
150
+ it('does not set externalSecrets when the yml has none', () => {
151
+ const env = parseEnvironment('name: test_env\nvariables: []\n');
152
+ expect(env.externalSecrets).toBeUndefined();
153
+ });
154
+
155
+ it('round-trips externalSecrets through parse -> stringify -> parse', () => {
156
+ const env = parseEnvironment(EXTERNAL_SECRETS_YML);
157
+ const reparsed = parseEnvironment(stringifyEnvironment(env));
158
+
159
+ expect(reparsed.externalSecrets).toEqual(env.externalSecrets);
160
+ });
161
+ });
@@ -0,0 +1,34 @@
1
+ import { toBrunoVariables, toOpenCollectionVariables } from '../common/variables';
2
+
3
+ describe('yml variables - typed values (collection / folder / request vars)', () => {
4
+ it('reads a typed value keeping the value as a string and the type in datatype', () => {
5
+ const ocVariables = [
6
+ { name: 'var_str', value: 'plain' },
7
+ { name: 'var_num', value: { type: 'number', data: '300' } },
8
+ { name: 'var_bool', value: { type: 'boolean', data: 'false' } },
9
+ { name: 'var_obj', value: { type: 'object', data: '{"scope":"folder"}' } }
10
+ ];
11
+
12
+ const { req } = toBrunoVariables(ocVariables);
13
+
14
+ expect(req.find((v) => v.name === 'var_str')).toMatchObject({ value: 'plain' });
15
+ expect(req.find((v) => v.name === 'var_str')).not.toHaveProperty('datatype');
16
+ expect(req.find((v) => v.name === 'var_num')).toMatchObject({ value: '300', datatype: 'number' });
17
+ expect(req.find((v) => v.name === 'var_bool')).toMatchObject({ value: 'false', datatype: 'boolean' });
18
+ expect(req.find((v) => v.name === 'var_obj')).toMatchObject({ value: '{"scope":"folder"}', datatype: 'object' });
19
+ });
20
+
21
+ it('writes datatype back as a { type, data } value, leaving plain strings untouched', () => {
22
+ const brunoVariables = [
23
+ { uid: 'u1', name: 'var_str', value: 'plain', enabled: true, local: false },
24
+ { uid: 'u2', name: 'var_num', value: '300', datatype: 'number', enabled: true, local: false }
25
+ ];
26
+
27
+ const ocVariables = toOpenCollectionVariables(brunoVariables);
28
+
29
+ expect(ocVariables).toEqual([
30
+ { name: 'var_str', value: 'plain' },
31
+ { name: 'var_num', value: { type: 'number', data: '300' } }
32
+ ]);
33
+ });
34
+ });