Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/oas-to-har/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { HEADERS, PROXY_ENABLED } from 'oas/extensions';
import { Operation } from 'oas/operation';
import { isRef } from 'oas/types';
import { jsonSchemaTypes, matchesMimeType } from 'oas/utils';
import removeUndefinedObjects from 'remove-undefined-objects';
import removeUndefinedObjects from './lib/remove-undefined-objects.js';

import configureSecurity from './lib/configure-security.js';
import { get, set } from './lib/lodash.js';
Expand Down Expand Up @@ -420,7 +420,8 @@ export default function oasToHar(

if (operation.isFormUrlEncoded()) {
if (Object.keys(formData.formData || {}).length) {
const cleanFormData = removeUndefinedObjects(JSON.parse(JSON.stringify(formData.formData)));
const cleanFormData = removeUndefinedObjects(formData.formData);

if (cleanFormData !== undefined) {
const postData: PostData = { params: [], mimeType: 'application/x-www-form-urlencoded' };

Expand All @@ -444,7 +445,7 @@ export default function oasToHar(

if (isMultipart || isJSON) {
try {
let cleanBody = removeUndefinedObjects(JSON.parse(JSON.stringify(formData.body)));
let cleanBody = removeUndefinedObjects(formData.body);

if (isMultipart) {
har.postData = { params: [], mimeType: 'multipart/form-data' };
Expand Down
116 changes: 116 additions & 0 deletions packages/oas-to-har/src/lib/remove-undefined-objects.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we vendoring and forking remove-undefined-objects in here instead of adding a new option to that library for preserving empty arrays? thats what @mjcuva and i talked about out what i thought needed to happen for this ticket

Copy link
Contributor Author

@eaglethrost eaglethrost Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this PR is not for that ticket, but a different one, which tries to solve a different issue of null values in arrays getting dropped in the request snippet.

The PR's related to the ticket you were referring to are these:

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
function isObject(obj: unknown) {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}

function isEmptyObject(obj: unknown) {
return typeof obj === 'object' && obj !== null && !Object.keys(obj).length;
}

interface RemovalOptions {
removeAllFalsy?: boolean;
}

// Modified from here: https://stackoverflow.com/a/43781499
function stripEmptyObjects(obj: any, options: RemovalOptions = {}) {
const cleanObj = obj;

if (obj === null && options.removeAllFalsy) {
return undefined;
}

if (!isObject(obj) && !Array.isArray(cleanObj)) {
return cleanObj;
}

if (!Array.isArray(cleanObj)) {

Object.keys(cleanObj).forEach(key => {
let value = cleanObj[key];

if (typeof value !== 'object') {
return;
}

if (value === null) {
if (options.removeAllFalsy) {
delete cleanObj[key];
}
return;
}

value = stripEmptyObjects(value, options);

if (isEmptyObject(value)) {
delete cleanObj[key];
} else {
cleanObj[key] = value;
}
});

return cleanObj;
}

cleanObj.forEach((o, idx) => {
let value = o;
if (typeof value === 'object' && value !== null) {
value = stripEmptyObjects(value, options);

if (isEmptyObject(value)) {
delete cleanObj[idx];
} else {
cleanObj[idx] = value;
}
} else if (value === null && options.removeAllFalsy) {
delete cleanObj[idx];
}
});

// Since deleting a key from an array will retain an undefined value in that array, we need to
// filter them out.
return cleanObj.filter(el => el !== undefined);
}

export default function removeUndefinedObjects<T>(obj?: T, options?: RemovalOptions): T | undefined {
if (obj === undefined) {
return undefined;
}

// Remove objects that recursively contain undefined values
// E.g. { a: { b: undefined } } -> { a: {} }
let withoutUndefined = removeUndefined(obj);

// Then we recursively remove all empty objects and nullish arrays
withoutUndefined = stripEmptyObjects(withoutUndefined, options);

// If the only thing that's leftover is an empty object then return nothing.
if (isEmptyObject(withoutUndefined)) return undefined;

return withoutUndefined;
}

function removeUndefined(obj: any): any {
if (obj === undefined) {
return undefined;
}
// Preserve null
if (obj === null) {
return null;
}
// Remove undefined in arrays
if (Array.isArray(obj)) {
return obj
.map(removeUndefined)
.filter(item => item !== undefined);
}
if (typeof obj === 'object') {
const cleaned: any = {};
for (const [key, value] of Object.entries(obj)) {
const cleanedValue = removeUndefined(value);
if (cleanedValue !== undefined) {
cleaned[key] = cleanedValue;
}
}
return cleaned;
}
return obj;
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,21 @@ function encodeArray({
escape,
isAllowedReserved = false,
}: Omit<StylizerConfig, 'value'> & { value: string[] }) {
const valueEncoder = (str: string) =>
encodeDisallowedCharacters(str, {
const valueEncoder = (str: string) => {
// Handle null values explicitly to prevent join() from converting to empty string
if (str === null) {
return 'null';
}

const result = encodeDisallowedCharacters(str, {
escape,
returnIfEncoded: location === 'query',
isAllowedReserved,
});

return result;
};

switch (style) {
/**
* @example <caption>`style: simple`</caption>
Expand Down
115 changes: 115 additions & 0 deletions packages/oas-to-har/test/lib/remove-undefined-objects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { describe, test, it, expect } from 'vitest';

import removeUndefinedObjects from '../../src/lib/remove-undefined-objects.js';

describe('typings', () => {
it('should not blow away typings from supplied objects', () => {
const obj = removeUndefinedObjects({
key: 'buster',
});

expect(obj).toBeDefined();
});
});

test('should leave primitives alone', () => {
expect(removeUndefinedObjects(1234)).toBe(1234);
expect(removeUndefinedObjects('1234')).toBe('1234');
expect(removeUndefinedObjects(null)).toBeNull();
expect(removeUndefinedObjects()).toBeUndefined();
expect(removeUndefinedObjects(undefined)).toBeUndefined();
});

test('should leave only truthy primitives alone when removeAllFalsy is true', () => {
expect(removeUndefinedObjects(1234, { removeAllFalsy: true })).toBe(1234);
expect(removeUndefinedObjects('1234', { removeAllFalsy: true })).toBe('1234');
expect(removeUndefinedObjects(null, { removeAllFalsy: true })).toBeUndefined();
expect(removeUndefinedObjects(undefined, { removeAllFalsy: true })).toBeUndefined();
});

test("should also remove '' and null values when removeAllFalsy is true", () => {
expect(removeUndefinedObjects({ value: 1234 }, { removeAllFalsy: true })).toStrictEqual({ value: 1234 });
expect(removeUndefinedObjects({ value: '1234' }, { removeAllFalsy: true })).toStrictEqual({ value: '1234' });
expect(removeUndefinedObjects({ value: null }, { removeAllFalsy: true })).toBeUndefined();
expect(removeUndefinedObjects({ value: undefined }, { removeAllFalsy: true })).toBeUndefined();
});

test('should remove empty objects with only empty properties', () => {
const obj = {
a: {
b: {},
c: {
d: {},
},
},
};

expect(removeUndefinedObjects(obj)).toBeUndefined();
});

test('should remove empty objects with only undefined properties', () => {
const obj = {
a: {
b: undefined,
c: {
d: undefined,
},
},
};

expect(removeUndefinedObjects(obj)).toBeUndefined();
});

test('should remove empty arrays from within object', () => {
const obj = {
a: {
b: undefined,
c: {
d: undefined,
},
},
d: [1234, undefined],
e: [],
f: null,
g: [null, undefined, null],
};

expect(removeUndefinedObjects(obj)).toStrictEqual({
d: [1234],
f: null,
g: [null, null],
});
});

test('should remove empty arrays and falsy values from within object when removeAllFalsy is true', () => {
const obj = {
a: {
b: undefined,
c: {
d: undefined,
},
},
d: [1234, undefined],
e: [],
f: null,
g: [null, undefined, null],
};

expect(removeUndefinedObjects(obj, { removeAllFalsy: true })).toStrictEqual({
d: [1234],
});
});

test('should remove undefined values from arrays & not null values', () => {
expect(removeUndefinedObjects([undefined, undefined])).toBeUndefined();
expect(removeUndefinedObjects([null])).toStrictEqual([null]);
expect(removeUndefinedObjects(['1234', null, undefined, { a: null, b: undefined }, ' ', ''])).toStrictEqual([
'1234',
null,
{
a: null,
},
' ',
'',
]);
});
4 changes: 2 additions & 2 deletions packages/oas-to-har/test/parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ describe('parameter handling', () => {
parameters: [{ name: 'id', in: 'query' }],
},
{ query: { id: [null, null] } },
[{ name: 'id', value: '&id=' }],
[{ name: 'id', value: 'null&id=null' }],
),
);

Expand Down Expand Up @@ -207,7 +207,7 @@ describe('parameter handling', () => {
],
},
{ query: {} },
[{ name: 'id', value: '&id=' }],
[{ name: 'id', value: 'null&id=null' }],
),
);

Expand Down