Skip to content

Commit 38b1a28

Browse files
authored
fix(lib-dynamodb): make command middleware useable, turn marshalling into middleware (#3808)
* fix(lib-dynamodb): make command middleware useable, turn marshalling into middleware * fix(lib-dynamodb): fix unit test * fix(lib-dynamodb): move inner command construction to ctor body * fix(lib-dynamodb): remove command name field * fix(lib-dynamodb): fix ctor code order
1 parent 59cdfd8 commit 38b1a28

File tree

18 files changed

+512
-262
lines changed

18 files changed

+512
-262
lines changed

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/DocumentClientCommandGenerator.java

Lines changed: 68 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515

1616
package software.amazon.smithy.aws.typescript.codegen;
1717

18-
import static software.amazon.smithy.aws.typescript.codegen.propertyaccess.PropertyAccessor.getFrom;
19-
2018
import java.nio.file.Paths;
2119
import java.util.ArrayList;
2220
import java.util.HashSet;
@@ -55,10 +53,14 @@ final class DocumentClientCommandGenerator implements Runnable {
5553
private final TypeScriptWriter writer;
5654
private final Symbol symbol;
5755
private final OperationIndex operationIndex;
56+
private final String originalInputTypeName;
5857
private final String inputTypeName;
5958
private final List<MemberShape> inputMembersWithAttr;
59+
private final String originalOutputTypeName;
6060
private final String outputTypeName;
6161
private final List<MemberShape> outputMembersWithAttr;
62+
private final String clientCommandClassName;
63+
private final String clientCommandLocalName;
6264

6365
DocumentClientCommandGenerator(
6466
TypeScriptSettings settings,
@@ -75,14 +77,21 @@ final class DocumentClientCommandGenerator implements Runnable {
7577

7678
symbol = symbolProvider.toSymbol(operation);
7779
operationIndex = OperationIndex.of(model);
80+
String inputType = symbol.expectProperty("inputType", Symbol.class).getName();
81+
originalInputTypeName = inputType;
7882
inputTypeName = DocumentClientUtils.getModifiedName(
79-
symbol.expectProperty("inputType", Symbol.class).getName()
83+
inputType
8084
);
8185
inputMembersWithAttr = getStructureMembersWithAttr(operationIndex.getInput(operation));
86+
String outputType = symbol.expectProperty("outputType", Symbol.class).getName();
87+
originalOutputTypeName = outputType;
8288
outputTypeName = DocumentClientUtils.getModifiedName(
83-
symbol.expectProperty("outputType", Symbol.class).getName()
89+
outputType
8490
);
8591
outputMembersWithAttr = getStructureMembersWithAttr(operationIndex.getOutput(operation));
92+
93+
clientCommandClassName = symbol.getName();
94+
clientCommandLocalName = "__" + clientCommandClassName;
8695
}
8796

8897
@Override
@@ -92,44 +101,65 @@ public void run() {
92101

93102
// Add required imports.
94103
writer.addImport(configType, configType, servicePath);
95-
writer.addImport("Command", "$Command", "@aws-sdk/smithy-client");
104+
writer.addImport(
105+
"DynamoDBDocumentClientCommand",
106+
"DynamoDBDocumentClientCommand",
107+
"./baseCommand/DynamoDBDocumentClientCommand"
108+
);
96109

97110
generateInputAndOutputTypes();
98111

112+
String ioTypes = String.join(", ", new String[]{
113+
inputTypeName,
114+
outputTypeName,
115+
"__" + originalInputTypeName,
116+
"__" + originalOutputTypeName
117+
});
118+
99119
String name = DocumentClientUtils.getModifiedName(symbol.getName());
100120
writer.writeDocs(DocumentClientUtils.getCommandDocs(symbol.getName()));
101-
writer.openBlock("export class $L extends $$Command<$L, $L, $L> {", "}",
102-
name, inputTypeName, outputTypeName, configType, () -> {
103-
104-
// Section for adding custom command properties.
105-
writer.pushState(COMMAND_PROPERTIES_SECTION);
106-
if (!inputMembersWithAttr.isEmpty()) {
107-
writer.openBlock("private readonly $L = [", "];", COMMAND_INPUT_KEYNODES, () -> {
121+
writer.openBlock(
122+
"export class $L extends DynamoDBDocumentClientCommand<" + ioTypes + ", $L> {",
123+
"}",
124+
name,
125+
configType,
126+
() -> {
127+
// Section for adding custom command properties.
128+
writer.pushState(COMMAND_PROPERTIES_SECTION);
129+
writer.openBlock("protected readonly $L = [", "];", COMMAND_INPUT_KEYNODES, () -> {
108130
writeKeyNodes(inputMembersWithAttr);
109131
});
110-
}
111-
if (!outputMembersWithAttr.isEmpty()) {
112-
writer.openBlock("private readonly $L = [", "];", COMMAND_OUTPUT_KEYNODES, () -> {
132+
writer.openBlock("protected readonly $L = [", "];", COMMAND_OUTPUT_KEYNODES, () -> {
113133
writeKeyNodes(outputMembersWithAttr);
114134
});
115-
}
116-
writer.popState();
117-
writer.write("");
135+
writer.popState();
136+
writer.write("");
137+
138+
writer.write("protected readonly clientCommand: $L;", clientCommandLocalName);
139+
writer.write(
140+
"public readonly middlewareStack: MiddlewareStack<$L>;",
141+
inputTypeName + " | __" + originalInputTypeName
142+
+ ", \n" + outputTypeName + " | __" + originalOutputTypeName
143+
);
144+
writer.write("");
118145

119-
generateCommandConstructor();
120-
writer.write("");
121-
generateCommandMiddlewareResolver(configType);
146+
generateCommandConstructor();
147+
writer.write("");
148+
generateCommandMiddlewareResolver(configType);
122149

123-
// Hook for adding more methods to the command.
124-
writer.pushState(COMMAND_BODY_EXTRA_SECTION).popState();
125-
});
150+
// Hook for adding more methods to the command.
151+
writer.pushState(COMMAND_BODY_EXTRA_SECTION).popState();
152+
}
153+
);
126154
}
127155

128156
private void generateCommandConstructor() {
129157
writer.openBlock("constructor(readonly input: $L) {", "}", inputTypeName, () -> {
130158
// The constructor can be intercepted and changed.
131159
writer.pushState(COMMAND_CONSTRUCTOR_SECTION)
132160
.write("super();")
161+
.write("this.clientCommand = new $L(this.input as any);", clientCommandLocalName)
162+
.write("this.middlewareStack = this.clientCommand.middlewareStack;")
133163
.popState();
134164
});
135165
}
@@ -155,52 +185,27 @@ private void generateCommandMiddlewareResolver(String configType) {
155185
.write("options?: $T", ApplicationProtocol.createDefaultHttpApplicationProtocol().getOptionsType())
156186
.dedent();
157187
writer.openBlock("): $L<$L, $L> {", "}", handler, inputTypeName, outputTypeName, () -> {
158-
String marshallOptions = DocumentClientUtils.CLIENT_MARSHALL_OPTIONS;
159-
String unmarshallOptions = DocumentClientUtils.CLIENT_UNMARSHALL_OPTIONS;
160-
161-
writer.write("const { $L, $L } = configuration.$L || {};", marshallOptions, unmarshallOptions,
162-
DocumentClientUtils.CLIENT_TRANSLATE_CONFIG_KEY);
163-
164-
writer.addImport(symbol.getName(), "__" + symbol.getName(), "@aws-sdk/client-dynamodb");
165-
166-
String marshallInput = "marshallInput";
167-
String unmarshallOutput = "unmarshallOutput";
168-
String utilsFileLocation = String.format("./%s/%s",
169-
DocumentClientUtils.CLIENT_COMMANDS_FOLDER, DocumentClientUtils.CLIENT_UTILS_FILE);
170-
writer.addImport(marshallInput, marshallInput, utilsFileLocation);
171-
writer.addImport(unmarshallOutput, unmarshallOutput, utilsFileLocation);
172-
173-
String commandVarName = "command";
174-
writer.openBlock("const $L = new $L(", ");", commandVarName, "__" + symbol.getName(),
175-
() -> {
176-
if (inputMembersWithAttr.isEmpty()) {
177-
writer.write("this.input,");
178-
} else {
179-
writer.openBlock("$L(", ")", marshallInput, () -> {
180-
writer.write("this.input,");
181-
writer.write(getFrom("this", COMMAND_INPUT_KEYNODES) + ",");
182-
writer.write("$L,", marshallOptions);
183-
});
184-
}
185-
});
188+
189+
writer.addImport(clientCommandClassName, clientCommandLocalName, "@aws-sdk/client-dynamodb");
190+
191+
String commandVarName = "this.clientCommand";
192+
193+
// marshall middlewares
194+
writer.openBlock("this.addMarshallingMiddleware(", ");", () -> {
195+
writer.write("configuration");
196+
});
197+
198+
writer.write("const stack = clientStack.concat(this.middlewareStack as typeof clientStack);");
199+
186200
String handlerVarName = "handler";
187-
writer.write("const $L = $L.resolveMiddleware(clientStack, configuration, options);",
201+
writer.write("const $L = $L.resolveMiddleware(stack, configuration, options);",
188202
handlerVarName, commandVarName);
189203
writer.write("");
190204

191205
if (outputMembersWithAttr.isEmpty()) {
192206
writer.write("return $L;", handlerVarName);
193207
} else {
194-
writer.openBlock("return async () => {", "};", () -> {
195-
String dataVarName = "data";
196-
String outputVarName = "output";
197-
writer.write("const $L = await $L($L);", dataVarName, handlerVarName, commandVarName);
198-
writer.openBlock("return {", "};", () -> {
199-
writer.write("...$L,", dataVarName);
200-
writer.write("$1L: $2L($3L.$1L, this.$4L, $5L),", outputVarName, unmarshallOutput,
201-
dataVarName, COMMAND_OUTPUT_KEYNODES, unmarshallOptions);
202-
});
203-
});
208+
writer.write("return async () => handler($L)", commandVarName);
204209
}
205210
});
206211
}
@@ -259,10 +264,8 @@ private void writeStructureKeyNode(StructureShape structureTarget) {
259264

260265
private void generateInputAndOutputTypes() {
261266
writer.write("");
262-
String originalInputTypeName = symbol.expectProperty("inputType", Symbol.class).getName();
263267
writeType(inputTypeName, originalInputTypeName, operationIndex.getInput(operation), inputMembersWithAttr);
264268
writer.write("");
265-
String originalOutputTypeName = symbol.expectProperty("outputType", Symbol.class).getName();
266269
writeType(outputTypeName, originalOutputTypeName, operationIndex.getOutput(operation), outputMembersWithAttr);
267270
writer.write("");
268271
}

lib/lib-dynamodb/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,79 @@ await ddbDocClient.put({
160160
});
161161
```
162162

163+
### Client and Command middleware stacks
164+
165+
As with other AWS SDK for JavaScript v3 clients, you can apply middleware functions
166+
both on the client itself and individual `Command`s.
167+
168+
For individual `Command`s, here are examples of how to add middleware before and after
169+
both marshalling and unmarshalling. We will use `QueryCommand` as an example.
170+
Others follow the same pattern.
171+
172+
```js
173+
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";
174+
175+
const client = new DynamoDBClient({
176+
/*...*/
177+
});
178+
const doc = DynamoDBDocumentClient.from(client);
179+
const command = new QueryCommand({
180+
/*...*/
181+
});
182+
```
183+
184+
Before and after marshalling:
185+
186+
```js
187+
command.middlewareStack.addRelativeTo(
188+
(next) => async (args) => {
189+
console.log("pre-marshall", args.input);
190+
return next(args);
191+
},
192+
{
193+
relation: "before",
194+
toMiddleware: "DocumentMarshall",
195+
}
196+
);
197+
command.middlewareStack.addRelativeTo(
198+
(next) => async (args) => {
199+
console.log("post-marshall", args.input);
200+
return next(args);
201+
},
202+
{
203+
relation: "after",
204+
toMiddleware: "DocumentMarshall",
205+
}
206+
);
207+
```
208+
209+
Before and after unmarshalling:
210+
211+
```js
212+
command.middlewareStack.addRelativeTo(
213+
(next) => async (args) => {
214+
const result = await next(args);
215+
console.log("pre-unmarshall", result.output.Items);
216+
return result;
217+
},
218+
{
219+
relation: "after", // <- after for pre-unmarshall
220+
toMiddleware: "DocumentUnmarshall",
221+
}
222+
);
223+
command.middlewareStack.addRelativeTo(
224+
(next) => async (args) => {
225+
const result = await next(args);
226+
console.log("post-unmarshall", result.output.Items);
227+
return result;
228+
},
229+
{
230+
relation: "before", // <- before for post-unmarshall
231+
toMiddleware: "DocumentUnmarshall",
232+
}
233+
);
234+
```
235+
163236
### Destroying document client
164237

165238
The `destroy()` call on document client is a no-op as document client does not
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Handler, MiddlewareStack } from "@aws-sdk/types";
2+
3+
import { KeyNode } from "../commands/utils";
4+
import { DynamoDBDocumentClientCommand } from "./DynamoDBDocumentClientCommand";
5+
6+
class AnyCommand extends DynamoDBDocumentClientCommand<{}, {}, {}, {}, {}> {
7+
public middlewareStack: MiddlewareStack<{}, {}>;
8+
public input: {};
9+
protected inputKeyNodes: KeyNode[] = [];
10+
protected outputKeyNodes: KeyNode[] = [];
11+
12+
public argCaptor: [Function, object][] = [];
13+
14+
protected readonly clientCommand = {
15+
middlewareStack: {
16+
argCaptor: this.argCaptor,
17+
add(fn, config) {
18+
this.argCaptor.push([fn, config]);
19+
},
20+
},
21+
} as any;
22+
protected readonly clientCommandName = "AnyCommand";
23+
24+
public constructor() {
25+
super();
26+
}
27+
28+
public resolveMiddleware(clientStack: MiddlewareStack<any, any>, configuration: {}, options: any): Handler<{}, {}> {
29+
this.addMarshallingMiddleware({} as any);
30+
return null as any;
31+
}
32+
}
33+
34+
describe("DynamoDBDocumentClientCommand", () => {
35+
it("should not allow usage of the default middlewareStack", () => {
36+
const command = new AnyCommand();
37+
command.resolveMiddleware(null as any, null as any, null as any);
38+
{
39+
const [middleware, options] = command.argCaptor[0];
40+
expect(middleware.toString()).toContain(`marshallInput`);
41+
expect(options).toEqual({
42+
name: "DocumentMarshall",
43+
override: true,
44+
step: "initialize",
45+
});
46+
}
47+
{
48+
const [middleware, options] = command.argCaptor[1];
49+
expect(middleware.toString()).toContain(`unmarshallOutput`);
50+
expect(options).toEqual({
51+
name: "DocumentUnmarshall",
52+
override: true,
53+
step: "deserialize",
54+
});
55+
}
56+
});
57+
});

0 commit comments

Comments
 (0)