Skip to content

Commit 3b3e30c

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

File tree

4 files changed

+79
-16
lines changed

4 files changed

+79
-16
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
"cross-fetch": "^3.1.5",
178178
"destroyable-server": "^1.0.2",
179179
"express": "^4.14.0",
180+
"fast-json-patch": "^3.1.1",
180181
"graphql": "^14.0.2 || ^15.5",
181182
"graphql-http": "^1.22.0",
182183
"graphql-subscriptions": "^1.1.0",

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import type * as net from 'net';
44
import { encode as encodeBase64 } from 'base64-arraybuffer';
55
import { Readable, Transform } from 'stream';
66
import { stripIndent } from 'common-tags';
7+
import {
8+
Operation as JsonPatchOperation,
9+
validate as validateJsonPatch
10+
} from 'fast-json-patch';
711

812
import {
913
Headers,
@@ -597,13 +601,25 @@ export interface RequestTransform {
597601

598602
/**
599603
* A JSON object which will be merged with the real request body. Undefined values
600-
* will be removed. Any requests which are received with an invalid JSON body that
601-
* match this rule will fail.
604+
* will be removed, and other values will be merged directly with the target value
605+
* recursively.
606+
*
607+
* Any requests which are received with an invalid JSON body that match this rule
608+
* will fail.
602609
*/
603610
updateJsonBody?: {
604611
[key: string]: any;
605612
};
606613

614+
/**
615+
* A series of operations to apply to the request body in JSON Patch format (RFC
616+
* 6902).
617+
*
618+
* Any requests which are received with an invalid JSON body that match this rule
619+
* will fail.
620+
*/
621+
patchJsonBody?: Array<JsonPatchOperation>;
622+
607623
/**
608624
* Perform a series of string match & replace operations on the request body.
609625
*
@@ -810,11 +826,17 @@ export class PassThroughHandlerDefinition extends Serializable implements Reques
810826
options.transformRequest.replaceBody,
811827
options.transformRequest.replaceBodyFromFile,
812828
options.transformRequest.updateJsonBody,
829+
options.transformRequest.patchJsonBody,
813830
options.transformRequest.matchReplaceBody
814831
].filter(o => !!o).length > 1) {
815832
throw new Error("Only one request body transform can be specified at a time");
816833
}
817834

835+
if (options.transformRequest.patchJsonBody) {
836+
const validationError = validateJsonPatch(options.transformRequest.patchJsonBody);
837+
if (validationError) throw validationError;
838+
}
839+
818840
this.transformRequest = options.transformRequest;
819841
}
820842

src/rules/requests/request-handlers.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { decode as decodeBase64 } from 'base64-arraybuffer';
1111
import { Transform } from 'stream';
1212
import { stripIndent, oneLine } from 'common-tags';
1313
import { TypedError } from 'typed-error';
14+
import { applyPatch as applyJsonPatch } from 'fast-json-patch';
1415

1516
import {
1617
Headers,
@@ -492,6 +493,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
492493
replaceBody,
493494
replaceBodyFromFile,
494495
updateJsonBody,
496+
patchJsonBody,
495497
matchReplaceBody
496498
} = this.transformRequest;
497499

@@ -518,22 +520,28 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
518520
reqBodyOverride = await fs.readFile(replaceBodyFromFile);
519521
} else if (updateJsonBody) {
520522
const { body: realBody } = await waitForCompletedRequest(clientReq);
521-
if (await realBody.getJson() === undefined) {
522-
throw new Error("Can't transform non-JSON request body");
523+
const jsonBody = await realBody.getJson();
524+
if (jsonBody === undefined) {
525+
throw new Error("Can't update JSON in non-JSON request body");
523526
}
524527

525-
const updatedBody = _.mergeWith(
526-
await realBody.getJson(),
527-
updateJsonBody,
528-
(_oldValue, newValue) => {
529-
// We want to remove values with undefines, but Lodash ignores
530-
// undefined return values here. Fortunately, JSON.stringify
531-
// ignores Symbols, omitting them from the result.
532-
if (newValue === undefined) return OMIT_SYMBOL;
533-
}
534-
);
528+
const updatedBody = _.mergeWith(jsonBody, updateJsonBody, (_oldValue, newValue) => {
529+
// We want to remove values with undefines, but Lodash ignores
530+
// undefined return values here. Fortunately, JSON.stringify
531+
// ignores Symbols, omitting them from the result.
532+
if (newValue === undefined) return OMIT_SYMBOL;
533+
});
535534

536535
reqBodyOverride = asBuffer(JSON.stringify(updatedBody));
536+
} else if (patchJsonBody) {
537+
const { body: realBody } = await waitForCompletedRequest(clientReq);
538+
const jsonBody = await realBody.getJson();
539+
if (jsonBody === undefined) {
540+
throw new Error("Can't patch JSON in non-JSON request body");
541+
}
542+
543+
applyJsonPatch(jsonBody, patchJsonBody, true); // Mutates the JSON body returned above
544+
reqBodyOverride = asBuffer(JSON.stringify(jsonBody));
537545
} else if (matchReplaceBody) {
538546
const { body: realBody } = await waitForCompletedRequest(clientReq);
539547

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,8 @@ nodeOnly(() => {
451451
it("can update a JSON body with new fields", async () => {
452452
await server.forAnyRequest().thenPassThrough({
453453
transformRequest: {
454-
updateJsonBody:{
454+
// Same update as the JSON Patch below, in simpler merge form:
455+
updateJsonBody: {
455456
a: 100, // Update
456457
b: undefined, // Remove
457458
c: 2 // Add
@@ -480,7 +481,8 @@ nodeOnly(() => {
480481
it("can update a JSON body while handling encoding automatically", async () => {
481482
await server.forAnyRequest().thenPassThrough({
482483
transformRequest: {
483-
updateJsonBody:{
484+
// Same update as the JSON Patch below, in simpler merge form:
485+
updateJsonBody: {
484486
a: 100, // Update
485487
b: undefined, // Remove
486488
c: 2 // Add
@@ -513,6 +515,36 @@ nodeOnly(() => {
513515
body: JSON.stringify({ a: 100, c: 2 })
514516
});
515517
});
518+
519+
it("can update a JSON body with a JSON patch", async () => {
520+
await server.forAnyRequest().thenPassThrough({
521+
transformRequest: {
522+
patchJsonBody: [
523+
// Same logic as the update above, in JSON Patch form:
524+
{ op: 'replace', path: '/a', value: 100 },
525+
{ op: 'remove', path: '/b' },
526+
{ op: 'add', path: '/c', value: 2 }
527+
]
528+
}
529+
});
530+
531+
let response = await request.post(remoteServer.urlFor("/abc"), {
532+
headers: { 'custom-header': 'a-value' },
533+
body: { a: 1, b: 2 },
534+
json: true
535+
});
536+
537+
expect(response).to.deep.equal({
538+
url: remoteServer.urlFor("/abc"),
539+
method: 'POST',
540+
headers: {
541+
...baseHeaders(),
542+
'content-length': '15',
543+
'custom-header': 'a-value'
544+
},
545+
body: JSON.stringify({ a: 100, c: 2 })
546+
});
547+
});
516548
});
517549

518550
describe("that transforms responses automatically", () => {

0 commit comments

Comments
 (0)