Skip to content

Commit b7e003a

Browse files
committed
Add support for JSON patch transforms on response bodies
1 parent 3b3e30c commit b7e003a

File tree

3 files changed

+75
-15
lines changed

3 files changed

+75
-15
lines changed

src/rules/requests/request-handler-definitions.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -672,13 +672,25 @@ export interface ResponseTransform {
672672

673673
/**
674674
* A JSON object which will be merged with the real response body. Undefined values
675-
* will be removed. Any responses which are received with an invalid JSON body that
676-
* match this rule will fail.
675+
* will be removed, and other values will be merged directly with the target value
676+
* recursively.
677+
*
678+
* Any responses which are received with an invalid JSON body that match this rule
679+
* will fail.
677680
*/
678681
updateJsonBody?: {
679682
[key: string]: any;
680683
};
681684

685+
/**
686+
* A series of operations to apply to the response body in JSON Patch format (RFC
687+
* 6902).
688+
*
689+
* Any responses which are received with an invalid JSON body that match this rule
690+
* will fail.
691+
*/
692+
patchJsonBody?: Array<JsonPatchOperation>;
693+
682694
/**
683695
* Perform a series of string match & replace operations on the response body.
684696
*
@@ -855,11 +867,17 @@ export class PassThroughHandlerDefinition extends Serializable implements Reques
855867
options.transformResponse.replaceBody,
856868
options.transformResponse.replaceBodyFromFile,
857869
options.transformResponse.updateJsonBody,
870+
options.transformResponse.patchJsonBody,
858871
options.transformResponse.matchReplaceBody
859872
].filter(o => !!o).length > 1) {
860873
throw new Error("Only one response body transform can be specified at a time");
861874
}
862875

876+
if (options.transformResponse.patchJsonBody) {
877+
const validationError = validateJsonPatch(options.transformResponse.patchJsonBody);
878+
if (validationError) throw validationError;
879+
}
880+
863881
this.transformResponse = options.transformResponse;
864882
}
865883
}

src/rules/requests/request-handlers.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
836836
replaceBody,
837837
replaceBodyFromFile,
838838
updateJsonBody,
839+
patchJsonBody,
839840
matchReplaceBody
840841
} = this.transformResponse;
841842

@@ -862,23 +863,31 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
862863
} else if (updateJsonBody) {
863864
originalBody = await streamToBuffer(serverRes);
864865
const realBody = buildBodyReader(originalBody, serverRes.headers);
866+
const jsonBody = await realBody.getJson();
865867

866-
if (await realBody.getJson() === undefined) {
867-
throw new Error("Can't transform non-JSON response body");
868+
if (jsonBody === undefined) {
869+
throw new Error("Can't update JSON in non-JSON response body");
868870
}
869871

870-
const updatedBody = _.mergeWith(
871-
await realBody.getJson(),
872-
updateJsonBody,
873-
(_oldValue, newValue) => {
874-
// We want to remove values with undefines, but Lodash ignores
875-
// undefined return values here. Fortunately, JSON.stringify
876-
// ignores Symbols, omitting them from the result.
877-
if (newValue === undefined) return OMIT_SYMBOL;
878-
}
879-
);
872+
const updatedBody = _.mergeWith(jsonBody, updateJsonBody, (_oldValue, newValue) => {
873+
// We want to remove values with undefines, but Lodash ignores
874+
// undefined return values here. Fortunately, JSON.stringify
875+
// ignores Symbols, omitting them from the result.
876+
if (newValue === undefined) return OMIT_SYMBOL;
877+
});
880878

881879
resBodyOverride = asBuffer(JSON.stringify(updatedBody));
880+
} else if (patchJsonBody) {
881+
originalBody = await streamToBuffer(serverRes);
882+
const realBody = buildBodyReader(originalBody, serverRes.headers);
883+
const jsonBody = await realBody.getJson();
884+
885+
if (jsonBody === undefined) {
886+
throw new Error("Can't patch JSON in non-JSON response body");
887+
}
888+
889+
applyJsonPatch(jsonBody, patchJsonBody, true); // Mutates the JSON body returned above
890+
resBodyOverride = asBuffer(JSON.stringify(jsonBody));
882891
} else if (matchReplaceBody) {
883892
originalBody = await streamToBuffer(serverRes);
884893
const realBody = buildBodyReader(originalBody, serverRes.headers);

test/integration/proxying/proxy-transforms.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,8 @@ nodeOnly(() => {
784784
it("can update a JSON body with new fields", async () => {
785785
await server.forAnyRequest().thenPassThrough({
786786
transformResponse: {
787-
updateJsonBody:{
787+
// Same update as the JSON Patch below, in simpler merge form:
788+
updateJsonBody: {
788789
'body-value': false, // Update
789790
'another-body-value': undefined, // Remove
790791
'new-value': 123 // Add
@@ -817,6 +818,7 @@ nodeOnly(() => {
817818
updateHeaders: {
818819
'content-encoding': 'br'
819820
},
821+
// Same update as the JSON Patch below, in simpler merge form:
820822
updateJsonBody:{
821823
'body-value': false, // Update
822824
'another-body-value': undefined, // Remove
@@ -853,6 +855,37 @@ nodeOnly(() => {
853855
});
854856
});
855857

858+
it("can update a JSON body with a JSON patch", async () => {
859+
await server.forAnyRequest().thenPassThrough({
860+
transformResponse: {
861+
patchJsonBody: [
862+
// Same logic as the update above, in JSON Patch form:
863+
{ op: 'replace', path: '/body-value', value: false },
864+
{ op: 'remove', path: '/another-body-value' },
865+
{ op: 'add', path: '/new-value', value: 123 }
866+
]
867+
}
868+
});
869+
870+
let response = await request.post(remoteServer.url, {
871+
resolveWithFullResponse: true,
872+
simple: false
873+
});
874+
875+
expect(response.statusCode).to.equal(200);
876+
expect(response.statusMessage).to.equal('OK');
877+
expect(response.headers).to.deep.equal({
878+
'content-type': 'application/json',
879+
'content-length': '36',
880+
'connection': 'keep-alive',
881+
'custom-response-header': 'custom-value'
882+
});
883+
expect(JSON.parse(response.body)).to.deep.equal({
884+
'body-value': false,
885+
'new-value': 123
886+
});
887+
});
888+
856889
});
857890
});
858891
});

0 commit comments

Comments
 (0)