Skip to content

Commit e8f71ec

Browse files
authored
feat(sdk): support tenantId in context.invoke() for tenant isolation (#467)
Add `tenantId` option to `InvokeConfig` so durable functions can invoke tenant-isolated Lambda functions. The `TenantId` is passed through to `ChainedInvokeOptions`, which the backend already supports.
1 parent f5fc6cd commit e8f71ec

File tree

6 files changed

+198
-0
lines changed

6 files changed

+198
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
[
2+
{
3+
"EventType": "ExecutionStarted",
4+
"EventId": 1,
5+
"Id": "583741d8-8da7-46b2-a96a-95250aa494d5",
6+
"EventTimestamp": "2026-03-05T17:38:24.487Z",
7+
"ExecutionStartedDetails": {
8+
"Input": {
9+
"Payload": "{\"functionName\":\"wait-named\",\"tenantId\":\"tenant-abc-123\"}"
10+
}
11+
}
12+
},
13+
{
14+
"EventType": "ChainedInvokeStarted",
15+
"SubType": "ChainedInvoke",
16+
"EventId": 2,
17+
"Id": "c4ca4238a0b92382",
18+
"EventTimestamp": "2026-03-05T17:38:24.491Z",
19+
"ChainedInvokeStartedDetails": {
20+
"FunctionName": "",
21+
"Input": {
22+
"Payload": ""
23+
},
24+
"DurableExecutionArn": ""
25+
}
26+
},
27+
{
28+
"EventType": "InvocationCompleted",
29+
"EventId": 3,
30+
"EventTimestamp": "2026-03-05T17:38:24.513Z",
31+
"InvocationCompletedDetails": {
32+
"StartTimestamp": "2026-03-05T17:38:24.487Z",
33+
"EndTimestamp": "2026-03-05T17:38:24.513Z",
34+
"Error": {},
35+
"RequestId": "d2173995-9714-481b-9e9d-3dbc76cab2b8"
36+
}
37+
},
38+
{
39+
"EventType": "ChainedInvokeSucceeded",
40+
"SubType": "ChainedInvoke",
41+
"EventId": 4,
42+
"Id": "c4ca4238a0b92382",
43+
"EventTimestamp": "2026-03-05T17:38:26.600Z",
44+
"ChainedInvokeSucceededDetails": {
45+
"Result": {
46+
"Payload": "\"wait finished\""
47+
}
48+
}
49+
},
50+
{
51+
"EventType": "InvocationCompleted",
52+
"EventId": 5,
53+
"EventTimestamp": "2026-03-05T17:38:26.603Z",
54+
"InvocationCompletedDetails": {
55+
"StartTimestamp": "2026-03-05T17:38:26.602Z",
56+
"EndTimestamp": "2026-03-05T17:38:26.603Z",
57+
"Error": {},
58+
"RequestId": "d3426f73-673f-4e40-a240-d56bb90d733c"
59+
}
60+
},
61+
{
62+
"EventType": "ExecutionSucceeded",
63+
"EventId": 6,
64+
"Id": "583741d8-8da7-46b2-a96a-95250aa494d5",
65+
"EventTimestamp": "2026-03-05T17:38:26.603Z",
66+
"ExecutionSucceededDetails": {
67+
"Result": {
68+
"Payload": "\"wait finished\""
69+
}
70+
}
71+
}
72+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing";
2+
import { createTests } from "../../../utils/test-helper";
3+
import { handler } from "./invoke-tenant-id";
4+
import { handler as namedWaitHandler } from "../../wait/named/wait-named";
5+
6+
createTests({
7+
handler,
8+
tests: function (runner, { functionNameMap, assertEventSignatures }) {
9+
it("should invoke with tenantId for tenant isolation", async () => {
10+
if (runner instanceof LocalDurableTestRunner) {
11+
runner.registerDurableFunction(
12+
functionNameMap.getFunctionName("wait-named"),
13+
namedWaitHandler,
14+
);
15+
}
16+
17+
const result = await runner.run({
18+
payload: {
19+
functionName: functionNameMap.getFunctionName("wait-named"),
20+
tenantId: "tenant-abc-123",
21+
},
22+
});
23+
expect(result.getResult()).toBe("wait finished");
24+
25+
assertEventSignatures(result, "tenant-id");
26+
});
27+
},
28+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {
2+
DurableContext,
3+
withDurableExecution,
4+
} from "@aws/durable-execution-sdk-js";
5+
import { ExampleConfig } from "../../../types";
6+
7+
export const config: ExampleConfig = {
8+
name: "Invoke Tenant Id",
9+
description:
10+
"Demonstrates invoking a tenant-isolated Lambda function using tenantId",
11+
};
12+
13+
export const handler = withDurableExecution(
14+
async (
15+
event: {
16+
functionName: string;
17+
tenantId: string;
18+
payload?: Record<string, unknown>;
19+
},
20+
context: DurableContext,
21+
) => {
22+
const result = await context.invoke(event.functionName, event.payload, {
23+
tenantId: event.tenantId,
24+
});
25+
return result;
26+
},
27+
);

packages/aws-durable-execution-sdk-js/src/handlers/invoke-handler/invoke-handler.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,74 @@ describe("InvokeHandler", () => {
391391
});
392392
});
393393

394+
it("should pass tenantId through ChainedInvokeOptions when provided", async () => {
395+
(mockContext.getStepData as jest.Mock)
396+
.mockReturnValueOnce(undefined)
397+
.mockReturnValue({
398+
Status: OperationStatus.SUCCEEDED,
399+
ChainedInvokeDetails: { Result: '{"result":"success"}' },
400+
});
401+
402+
mockSafeDeserialize.mockResolvedValue({ result: "success" });
403+
404+
const invokeHandler = createInvokeHandler(
405+
mockContext,
406+
mockCheckpoint,
407+
mockCreateStepId,
408+
"parent-123",
409+
);
410+
411+
const result = await invokeHandler(
412+
"test-function",
413+
{ test: "data" },
414+
{ tenantId: "tenant-abc-123" },
415+
);
416+
417+
expect(result).toEqual({ result: "success" });
418+
expect(mockCheckpoint.checkpoint).toHaveBeenCalledWith("test-step-1", {
419+
Id: "test-step-1",
420+
ParentId: "parent-123",
421+
Action: OperationAction.START,
422+
SubType: OperationSubType.CHAINED_INVOKE,
423+
Type: OperationType.CHAINED_INVOKE,
424+
Name: undefined,
425+
Payload: '{"serialized":"data"}',
426+
ChainedInvokeOptions: {
427+
FunctionName: "test-function",
428+
TenantId: "tenant-abc-123",
429+
},
430+
});
431+
});
432+
433+
it("should not include TenantId in ChainedInvokeOptions when not provided", async () => {
434+
(mockContext.getStepData as jest.Mock)
435+
.mockReturnValueOnce(undefined)
436+
.mockReturnValue({
437+
Status: OperationStatus.SUCCEEDED,
438+
ChainedInvokeDetails: { Result: '{"result":"success"}' },
439+
});
440+
441+
mockSafeDeserialize.mockResolvedValue({ result: "success" });
442+
443+
const invokeHandler = createInvokeHandler(
444+
mockContext,
445+
mockCheckpoint,
446+
mockCreateStepId,
447+
"parent-123",
448+
);
449+
450+
await invokeHandler("test-function", { test: "data" });
451+
452+
const checkpointCall = (mockCheckpoint.checkpoint as jest.Mock).mock
453+
.calls[0][1];
454+
expect(checkpointCall.ChainedInvokeOptions).toEqual({
455+
FunctionName: "test-function",
456+
});
457+
expect(checkpointCall.ChainedInvokeOptions).not.toHaveProperty(
458+
"TenantId",
459+
);
460+
});
461+
394462
it("should handle invoke with custom serdes", async () => {
395463
(mockContext.getStepData as jest.Mock)
396464
.mockReturnValueOnce(undefined)

packages/aws-durable-execution-sdk-js/src/handlers/invoke-handler/invoke-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export const createInvokeHandler = (
158158
Payload: serializedPayload,
159159
ChainedInvokeOptions: {
160160
FunctionName: funcId,
161+
...(config?.tenantId && { TenantId: config.tenantId }),
161162
},
162163
});
163164
}

packages/aws-durable-execution-sdk-js/src/types/invoke.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ export interface InvokeConfig<I, O> {
99
payloadSerdes?: Serdes<I>;
1010
/** Serialization/deserialization configuration for result data */
1111
resultSerdes?: Serdes<O>;
12+
/** Tenant identifier for invoking tenant-isolated Lambda functions */
13+
tenantId?: string;
1214
}

0 commit comments

Comments
 (0)