@usebruno/filestore 0.9.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.
Files changed (113) 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.map +1 -1
  22. package/dist/formats/yml/items/parseApp.d.ts +13 -0
  23. package/dist/formats/yml/items/parseApp.d.ts.map +1 -0
  24. package/dist/formats/yml/items/parseGraphQLRequest.d.ts.map +1 -1
  25. package/dist/formats/yml/items/parseGrpcRequest.d.ts.map +1 -1
  26. package/dist/formats/yml/items/parseHttpRequest.d.ts.map +1 -1
  27. package/dist/formats/yml/items/parseWebsocketRequest.d.ts.map +1 -1
  28. package/dist/formats/yml/items/stringifyApp.d.ts +4 -0
  29. package/dist/formats/yml/items/stringifyApp.d.ts.map +1 -0
  30. package/dist/formats/yml/items/stringifyGraphQLRequest.d.ts.map +1 -1
  31. package/dist/formats/yml/items/stringifyGrpcRequest.d.ts.map +1 -1
  32. package/dist/formats/yml/items/stringifyHttpRequest.d.ts.map +1 -1
  33. package/dist/formats/yml/items/stringifyWebsocketRequest.d.ts.map +1 -1
  34. package/dist/formats/yml/parseCollection.d.ts.map +1 -1
  35. package/dist/formats/yml/parseEnvironment.d.ts +3 -1
  36. package/dist/formats/yml/parseEnvironment.d.ts.map +1 -1
  37. package/dist/formats/yml/parseFolder.d.ts.map +1 -1
  38. package/dist/formats/yml/parseItem.d.ts.map +1 -1
  39. package/dist/formats/yml/stringifyCollection.d.ts.map +1 -1
  40. package/dist/formats/yml/stringifyEnvironment.d.ts +3 -1
  41. package/dist/formats/yml/stringifyEnvironment.d.ts.map +1 -1
  42. package/dist/formats/yml/stringifyItem.d.ts.map +1 -1
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/types.d.ts +6 -0
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/utils/secretsJsonToEnvironments.d.ts +38 -0
  48. package/dist/utils/secretsJsonToEnvironments.d.ts.map +1 -0
  49. package/package.json +7 -5
  50. package/src/constants.ts +9 -0
  51. package/src/formats/bru/index.ts +52 -0
  52. package/src/formats/bru/tests/parseBruEnvironment.spec.js +63 -0
  53. package/src/formats/bru/tests/redact-large-text-blocks.spec.js +313 -0
  54. package/src/formats/bru/utils/redact-large-text-blocks.ts +102 -0
  55. package/src/formats/yml/common/actions.ts +1 -1
  56. package/src/formats/yml/common/app.ts +27 -0
  57. package/src/formats/yml/common/auth.ts +53 -1
  58. package/src/formats/yml/common/body.spec.ts +62 -0
  59. package/src/formats/yml/common/body.ts +27 -9
  60. package/src/formats/yml/common/datatype.spec.ts +110 -0
  61. package/src/formats/yml/common/datatype.ts +57 -0
  62. package/src/formats/yml/common/headers.ts +4 -0
  63. package/src/formats/yml/common/variables.spec.ts +127 -0
  64. package/src/formats/yml/common/variables.ts +20 -6
  65. package/src/formats/yml/items/parseApp.ts +37 -0
  66. package/src/formats/yml/items/parseGraphQLRequest.ts +8 -0
  67. package/src/formats/yml/items/parseGrpcRequest.ts +24 -3
  68. package/src/formats/yml/items/parseHttpRequest.ts +13 -0
  69. package/src/formats/yml/items/parseWebsocketRequest.ts +29 -9
  70. package/src/formats/yml/items/stringifyApp.ts +45 -0
  71. package/src/formats/yml/items/stringifyGraphQLRequest.ts +3 -0
  72. package/src/formats/yml/items/stringifyGrpcRequest.ts +9 -11
  73. package/src/formats/yml/items/stringifyHttpRequest.ts +10 -0
  74. package/src/formats/yml/items/stringifyWebsocketRequest.ts +22 -9
  75. package/src/formats/yml/parseCollection.spec.ts +70 -0
  76. package/src/formats/yml/parseCollection.ts +8 -3
  77. package/src/formats/yml/parseEnvironment.spec.ts +99 -0
  78. package/src/formats/yml/parseEnvironment.ts +69 -5
  79. package/src/formats/yml/parseFolder.spec.ts +52 -0
  80. package/src/formats/yml/parseFolder.ts +14 -21
  81. package/src/formats/yml/parseItem.spec.ts +147 -0
  82. package/src/formats/yml/parseItem.ts +4 -0
  83. package/src/formats/yml/stringifyCollection.spec.ts +77 -0
  84. package/src/formats/yml/stringifyCollection.ts +4 -0
  85. package/src/formats/yml/stringifyEnvironment.spec.ts +91 -0
  86. package/src/formats/yml/stringifyEnvironment.ts +59 -10
  87. package/src/formats/yml/stringifyFolder.spec.ts +47 -0
  88. package/src/formats/yml/stringifyItem.spec.ts +53 -0
  89. package/src/formats/yml/stringifyItem.ts +4 -0
  90. package/src/formats/yml/tests/fixtures/presets/empty-request.yml +7 -0
  91. package/src/formats/yml/tests/fixtures/presets/no-presets.yml +3 -0
  92. package/src/formats/yml/tests/fixtures/presets/no-request-key.yml +6 -0
  93. package/src/formats/yml/tests/fixtures/presets/type-only-realistic.yml +24 -0
  94. package/src/formats/yml/tests/fixtures/presets/url-only-realistic.yml +25 -0
  95. package/src/formats/yml/tests/fixtures/presets/with-type-and-url.yml +9 -0
  96. package/src/formats/yml/tests/parseCollection.spec.js +63 -0
  97. package/src/formats/yml/tests/parseEnvironment.spec.js +122 -0
  98. package/src/formats/yml/tests/parseEnvironment.spec.ts +160 -0
  99. package/src/formats/yml/tests/stringifyEnvironment.spec.ts +204 -0
  100. package/src/formats/yml/tests/variables.spec.js +34 -0
  101. package/src/index.ts +5 -0
  102. package/src/types/bruno-lang.d.ts +1 -0
  103. package/src/types.ts +7 -0
  104. package/src/utils/secretsJsonToEnvironments.spec.ts +211 -0
  105. package/src/utils/secretsJsonToEnvironments.ts +129 -0
  106. package/dist/formats/bru/tests/example-status-swap.spec.d.ts +0 -2
  107. package/dist/formats/bru/tests/example-status-swap.spec.d.ts.map +0 -1
  108. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts +0 -57
  109. package/dist/formats/bru/tests/fixtures/oauth2-additional-params.d.ts.map +0 -1
  110. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts +0 -2
  111. package/dist/formats/bru/tests/oauth2-additional-params.spec.d.ts.map +0 -1
  112. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts +0 -2
  113. package/dist/formats/bru/tests/request-parse-and-redact-body-data.spec.d.ts.map +0 -1
@@ -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,46 +1,89 @@
1
- import type { Environment as BrunoEnvironment, EnvironmentVariable as BrunoEnvironmentVariable } from '@usebruno/schema-types/collection/environment';
1
+ import type {
2
+ Environment as BrunoEnvironment,
3
+ EnvironmentVariable as BrunoEnvironmentVariable,
4
+ SecretEnvironmentVariable as BrunoSecretEnvironmentVariable,
5
+ ExternalSecretProviderType as BrunoExternalSecretProviderType
6
+ } from '@usebruno/schema-types/collection/environment';
2
7
  import type { Environment } from '@opencollection/types/config/environments';
3
8
  import type { Variable, SecretVariable } from '@opencollection/types/common/variables';
4
9
  import { stringifyYml } from './utils';
10
+ import { ensureString } from '../../utils';
11
+ import { SECRET_PROVIDER_IDENTIFIER_FIELDS } from '../../constants';
12
+ import { hasTypedMetadata, toOpenCollectionTypedValue, serializeVariableValue } from './common/datatype';
5
13
 
6
- const toOpenCollectionEnvironmentVariables = (variables: BrunoEnvironmentVariable[]): (Variable | SecretVariable)[] | undefined => {
14
+ export const toOpenCollectionEnvironmentVariables = (variables: (BrunoEnvironmentVariable | BrunoSecretEnvironmentVariable)[]): (Variable | SecretVariable)[] | undefined => {
7
15
  if (!variables?.length) {
8
16
  return undefined;
9
17
  }
10
18
 
11
19
  const ocVariables: (Variable | SecretVariable)[] = variables
12
- .filter((v: BrunoEnvironmentVariable) => {
13
- // todo: currently neither bru lang nor bruno app supports non-string values
14
- // update this when bruno app supports non-string values
15
- return typeof v.value === 'string';
16
- })
17
- .map((v: BrunoEnvironmentVariable): Variable | SecretVariable => {
18
- if (v.secret === true) {
20
+ .map((v): Variable | SecretVariable => {
21
+ if ('secret' in v && v.secret === true) {
19
22
  const secretVar: SecretVariable = {
20
23
  secret: true,
21
24
  name: v.name || ''
22
25
  };
26
+ if (hasTypedMetadata(v)) {
27
+ secretVar.type = v.dataType;
28
+ }
23
29
  if (v.enabled === false) {
24
30
  secretVar.disabled = true;
25
31
  }
32
+ if (v.description !== undefined) {
33
+ secretVar.description = v.description;
34
+ }
26
35
  return secretVar;
27
36
  }
28
37
 
38
+ const valueStr = serializeVariableValue(v.value);
39
+
29
40
  const variable: Variable = {
30
41
  name: v.name || '',
31
- value: v.value as string
42
+ value: hasTypedMetadata(v) ? toOpenCollectionTypedValue(v, valueStr) : valueStr
32
43
  };
33
44
 
34
45
  if (v.enabled === false) {
35
46
  variable.disabled = true;
36
47
  }
37
48
 
49
+ if (v.description !== undefined) {
50
+ variable.description = v.description;
51
+ }
52
+
38
53
  return variable;
39
54
  });
40
55
 
41
56
  return ocVariables.length > 0 ? ocVariables : undefined;
42
57
  };
43
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
+
44
87
  const stringifyEnvironment = (environment: BrunoEnvironment): string => {
45
88
  try {
46
89
  const ocEnvironment: Environment = {
@@ -58,10 +101,16 @@ const stringifyEnvironment = (environment: BrunoEnvironment): string => {
58
101
  }
59
102
  }
60
103
 
104
+ const ocExternalSecrets = toOpenCollectionExternalSecrets(environment.externalSecrets);
105
+ if (ocExternalSecrets) {
106
+ (ocEnvironment as any).externalSecrets = ocExternalSecrets;
107
+ }
108
+
61
109
  return stringifyYml(ocEnvironment);
62
110
  } catch (error) {
63
111
  console.error('Error stringifying environment:', error);
64
112
  throw error;
65
113
  }
66
114
  };
115
+
67
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,7 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
4
+ extensions:
5
+ bruno:
6
+ presets:
7
+ request: {}
@@ -0,0 +1,3 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
@@ -0,0 +1,6 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
4
+ extensions:
5
+ bruno:
6
+ presets: {}
@@ -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
@@ -0,0 +1,9 @@
1
+ opencollection: 1.0.0
2
+ info:
3
+ name: My Collection
4
+ extensions:
5
+ bruno:
6
+ presets:
7
+ request:
8
+ type: graphql
9
+ url: https://example.com/graphql
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import parseCollection from '../parseCollection';
4
+
5
+ const loadFixture = (name) =>
6
+ fs.readFileSync(path.join(__dirname, 'fixtures', 'presets', `${name}.yml`), 'utf8');
7
+
8
+ describe('yml parseCollection - presets', () => {
9
+ it('parses presets when request.type and request.url are present', () => {
10
+ const { brunoConfig } = parseCollection(loadFixture('with-type-and-url'));
11
+
12
+ expect(brunoConfig.presets).toEqual({
13
+ requestType: 'graphql',
14
+ requestUrl: 'https://example.com/graphql'
15
+ });
16
+ });
17
+
18
+ it('defaults requestType and requestUrl to empty strings (not arrays) when request fields are missing', () => {
19
+ const { brunoConfig } = parseCollection(loadFixture('empty-request'));
20
+
21
+ expect(brunoConfig.presets).toEqual({
22
+ requestType: '',
23
+ requestUrl: ''
24
+ });
25
+ expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
26
+ expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
27
+ });
28
+
29
+ it('does not set presets when the extension has no presets block', () => {
30
+ const { brunoConfig } = parseCollection(loadFixture('no-presets'));
31
+
32
+ expect(brunoConfig.presets).toBeUndefined();
33
+ });
34
+
35
+ it('does not set presets when presets exists but request key is absent', () => {
36
+ const { brunoConfig } = parseCollection(loadFixture('no-request-key'));
37
+
38
+ expect(brunoConfig.presets).toBeUndefined();
39
+ });
40
+
41
+ it('parses a realistic collection with only request.type set (no url) — defaults url to empty string', () => {
42
+ const { brunoConfig } = parseCollection(loadFixture('type-only-realistic'));
43
+
44
+ expect(brunoConfig.presets).toEqual({
45
+ requestType: 'graphql',
46
+ requestUrl: ''
47
+ });
48
+ expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
49
+ expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
50
+ });
51
+
52
+ it('parses a realistic collection with request.type http and request.url set', () => {
53
+ const { brunoConfig } = parseCollection(loadFixture('url-only-realistic'));
54
+
55
+ expect(brunoConfig.presets).toEqual({
56
+ requestType: 'http',
57
+ requestUrl: 'https://example.com/graphql'
58
+ });
59
+ expect(Array.isArray(brunoConfig.presets.requestType)).toBe(false);
60
+ expect(Array.isArray(brunoConfig.presets.requestUrl)).toBe(false);
61
+ expect(brunoConfig.ignore).toEqual(['node_modules', '.git']);
62
+ });
63
+ });