Skip to content

Commit 70213d7

Browse files
authored
DX-2133: add stringifyBody parameter to context.call and context.invoke (#131)
* feat: add stringifyBody parameter to context.call and context.invoke in both methods, stringifyBody is true by default. I also added tests to both method to check for different behaviors. * fix: require body field * fix: stringifyBody in context.call with workflow
1 parent 92efb18 commit 70213d7

File tree

8 files changed

+302
-15
lines changed

8 files changed

+302
-15
lines changed

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const NO_CONCURRENCY = 1;
1717
export const NOT_SET = "not-set";
1818
export const DEFAULT_RETRIES = 3;
1919

20-
export const VERSION = "v0.2.18";
20+
export const VERSION = "v0.2.20";
2121
export const SDK_TELEMETRY = `@upstash/workflow@${VERSION}`;
2222

2323
export const TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk" as const;

src/context/context.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ describe("context tests", () => {
337337
method: "POST",
338338
url: `${MOCK_QSTASH_SERVER_URL}/v2/batch`,
339339
token,
340+
headers: {
341+
"content-type": "application/json",
342+
},
340343
body: [
341344
{
342345
body: '"request-body"',
@@ -439,6 +442,94 @@ describe("context tests", () => {
439442
},
440443
});
441444
});
445+
test("should send context.call without parsing body if stringifyBody is false", async () => {
446+
const context = new WorkflowContext({
447+
qstashClient,
448+
initialPayload: "my-payload",
449+
steps: [],
450+
url: WORKFLOW_ENDPOINT,
451+
headers: new Headers() as Headers,
452+
workflowRunId: "wfr-id",
453+
});
454+
await mockQStashServer({
455+
execute: () => {
456+
const throws = () =>
457+
context.call("my-step", {
458+
url,
459+
body,
460+
headers: { "my-header": "my-value" },
461+
method: "PATCH",
462+
stringifyBody: false,
463+
});
464+
expect(throws).toThrowError("Aborting workflow after executing step 'my-step'.");
465+
},
466+
responseFields: {
467+
status: 200,
468+
body: "msgId",
469+
},
470+
receivesRequest: {
471+
method: "POST",
472+
url: `${MOCK_QSTASH_SERVER_URL}/v2/batch`,
473+
token,
474+
body: [
475+
{
476+
body: "request-body",
477+
destination: url,
478+
headers: {
479+
"upstash-workflow-sdk-version": "1",
480+
"content-type": "application/json",
481+
"upstash-callback": WORKFLOW_ENDPOINT,
482+
"upstash-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger",
483+
"upstash-callback-forward-upstash-workflow-callback": "true",
484+
"upstash-callback-forward-upstash-workflow-concurrent": "1",
485+
"upstash-callback-forward-upstash-workflow-contenttype": "application/json",
486+
"upstash-callback-forward-upstash-workflow-stepid": "1",
487+
"upstash-callback-forward-upstash-workflow-stepname": "my-step",
488+
"upstash-callback-forward-upstash-workflow-steptype": "Call",
489+
"upstash-callback-workflow-calltype": "fromCallback",
490+
"upstash-callback-workflow-init": "false",
491+
"upstash-callback-workflow-runid": "wfr-id",
492+
"upstash-callback-workflow-url": WORKFLOW_ENDPOINT,
493+
"upstash-feature-set": "WF_NoDelete,InitialBody",
494+
"upstash-forward-my-header": "my-value",
495+
"upstash-method": "PATCH",
496+
"upstash-retries": "0",
497+
"upstash-workflow-calltype": "toCallback",
498+
"upstash-workflow-init": "false",
499+
"upstash-workflow-runid": "wfr-id",
500+
"upstash-workflow-url": WORKFLOW_ENDPOINT,
501+
},
502+
},
503+
],
504+
},
505+
});
506+
});
507+
508+
test("should throw error if stringifyBody is false and body is object", async () => {
509+
const context = new WorkflowContext({
510+
qstashClient,
511+
initialPayload: "my-payload",
512+
steps: [],
513+
url: WORKFLOW_ENDPOINT,
514+
headers: new Headers() as Headers,
515+
workflowRunId: "wfr-id",
516+
});
517+
518+
const throws = () =>
519+
context.call("my-step", {
520+
url,
521+
body: { foo: "bar" },
522+
headers: { "my-header": "my-value" },
523+
method: "PATCH",
524+
// @ts-expect-error testing error case
525+
stringifyBody: false,
526+
});
527+
expect(throws).toThrowError(
528+
new WorkflowError(
529+
`When stringifyBody is false, body must be a string. Please check the body type of your call step.`
530+
)
531+
);
532+
});
442533
});
443534

444535
test("cancel should throw abort with cleanup: true", async () => {

src/context/context.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,11 @@ export class WorkflowContext<TInitialPayload = unknown> {
385385
| CallSettings<TBody>
386386
| (LazyInvokeStepParams<TBody, unknown> & Pick<CallSettings, "timeout">)
387387
): Promise<CallResponse<TResult | { workflowRunId: string }>> {
388-
let callStep: LazyCallStep<TResult | { workflowRunId: string }>;
388+
let callStep: LazyCallStep<TResult | { workflowRunId: string }, typeof settings.body>;
389389
if ("workflow" in settings) {
390390
const url = getNewUrlFromWorkflowId(this.url, settings.workflow.workflowId);
391391

392-
callStep = new LazyCallStep<{ workflowRunId: string }>(
392+
callStep = new LazyCallStep<{ workflowRunId: string }, typeof settings.body>(
393393
stepName,
394394
url,
395395
"POST",
@@ -398,7 +398,8 @@ export class WorkflowContext<TInitialPayload = unknown> {
398398
settings.retries || 0,
399399
settings.retryDelay,
400400
settings.timeout,
401-
settings.flowControl ?? settings.workflow.options.flowControl
401+
settings.flowControl ?? settings.workflow.options.flowControl,
402+
settings.stringifyBody ?? true
402403
);
403404
} else {
404405
const {
@@ -410,9 +411,10 @@ export class WorkflowContext<TInitialPayload = unknown> {
410411
retryDelay,
411412
timeout,
412413
flowControl,
414+
stringifyBody = true,
413415
} = settings;
414416

415-
callStep = new LazyCallStep<TResult>(
417+
callStep = new LazyCallStep<TResult, typeof body>(
416418
stepName,
417419
url,
418420
method,
@@ -421,7 +423,8 @@ export class WorkflowContext<TInitialPayload = unknown> {
421423
retries,
422424
retryDelay,
423425
timeout,
424-
flowControl
426+
flowControl,
427+
stringifyBody
425428
);
426429
}
427430

@@ -509,7 +512,7 @@ export class WorkflowContext<TInitialPayload = unknown> {
509512
stepName: string,
510513
settings: LazyInvokeStepParams<TInitialPayload, TResult>
511514
) {
512-
return await this.addStep(new LazyInvokeStep(stepName, settings));
515+
return await this.addStep(new LazyInvokeStep<TResult, TInitialPayload>(stepName, settings));
513516
}
514517

515518
/**

src/context/steps.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ describe("test steps", () => {
159159
14,
160160
"1000",
161161
30,
162-
flowControl
162+
flowControl,
163+
true
163164
);
164165

165166
test("should set correct fields", () => {

src/context/steps.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ export class LazyCallStep<TResult = unknown, TBody = unknown> extends BaseLazySt
333333
public readonly retryDelay?: string;
334334
public readonly timeout?: number | Duration;
335335
public readonly flowControl?: FlowControl;
336+
private readonly stringifyBody: boolean;
337+
336338
stepType: StepType = "Call";
337339
allowUndefinedOut = false;
338340

@@ -345,7 +347,8 @@ export class LazyCallStep<TResult = unknown, TBody = unknown> extends BaseLazySt
345347
retries: number,
346348
retryDelay: string | undefined,
347349
timeout: number | Duration | undefined,
348-
flowControl: FlowControl | undefined
350+
flowControl: FlowControl | undefined,
351+
stringifyBody: boolean
349352
) {
350353
super(stepName);
351354
this.url = url;
@@ -356,6 +359,7 @@ export class LazyCallStep<TResult = unknown, TBody = unknown> extends BaseLazySt
356359
this.retryDelay = retryDelay;
357360
this.timeout = timeout;
358361
this.flowControl = flowControl;
362+
this.stringifyBody = stringifyBody;
359363
}
360364

361365
public getPlanStep(concurrent: number, targetStep: number): Step<undefined> {
@@ -495,10 +499,23 @@ export class LazyCallStep<TResult = unknown, TBody = unknown> extends BaseLazySt
495499
}
496500

497501
async submitStep({ context, headers }: SubmitStepParams) {
502+
let callBody: string;
503+
if (this.stringifyBody) {
504+
callBody = JSON.stringify(this.body);
505+
} else {
506+
if (typeof this.body === "string") {
507+
callBody = this.body;
508+
} else {
509+
throw new WorkflowError(
510+
"When stringifyBody is false, body must be a string. Please check the body type of your call step."
511+
);
512+
}
513+
}
514+
498515
return (await context.qstashClient.batch([
499516
{
500517
headers,
501-
body: JSON.stringify(this.body),
518+
body: callBody,
502519
method: this.method,
503520
url: this.url,
504521
retries: DEFAULT_RETRIES === this.retries ? undefined : this.retries,
@@ -650,7 +667,7 @@ export class LazyInvokeStep<TResult = unknown, TBody = unknown> extends BaseLazy
650667
stepType: StepType = "Invoke";
651668
params: RequiredExceptFields<
652669
LazyInvokeStepParams<TBody, TResult>,
653-
"retries" | "flowControl" | "retryDelay"
670+
"retries" | "flowControl" | "retryDelay" | "body"
654671
>;
655672
protected allowUndefinedOut = false;
656673
/**
@@ -668,6 +685,7 @@ export class LazyInvokeStep<TResult = unknown, TBody = unknown> extends BaseLazy
668685
retries,
669686
retryDelay,
670687
flowControl,
688+
stringifyBody = true,
671689
}: LazyInvokeStepParams<TBody, TResult>
672690
) {
673691
super(stepName);
@@ -679,6 +697,7 @@ export class LazyInvokeStep<TResult = unknown, TBody = unknown> extends BaseLazy
679697
retries,
680698
retryDelay,
681699
flowControl,
700+
stringifyBody,
682701
};
683702

684703
const { workflowId } = workflow;
@@ -740,8 +759,21 @@ export class LazyInvokeStep<TResult = unknown, TBody = unknown> extends BaseLazy
740759
});
741760
invokerHeaders["Upstash-Workflow-Runid"] = context.workflowRunId;
742761

762+
let invokeBody: string;
763+
if (this.params.stringifyBody) {
764+
invokeBody = JSON.stringify(this.params.body);
765+
} else {
766+
if (typeof this.params.body === "string") {
767+
invokeBody = this.params.body;
768+
} else {
769+
throw new WorkflowError(
770+
"When stringifyBody is false, body must be a string. Please check the body type of your invoke step."
771+
);
772+
}
773+
}
774+
743775
const request: InvokeWorkflowRequest = {
744-
body: JSON.stringify(this.params.body),
776+
body: invokeBody,
745777
headers: Object.fromEntries(
746778
Object.entries(invokerHeaders).map((pairs) => [pairs[0], [pairs[1]]])
747779
),

0 commit comments

Comments
 (0)