Skip to content

Commit 7811cd9

Browse files
authored
fix(types): add client config interface test with s3 example (#4156)
* fix(types): add client config interface test with s3 example * fix(types): decouple V1orV2Endpoint type from EndpointBearer type * fix(lib-storage): type fix, allow undefined endpoint * fix(types): code reorganization in client-api-test
1 parent 302e5b2 commit 7811cd9

19 files changed

+384
-16
lines changed

Diff for: lib/lib-storage/src/Upload.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export class Upload extends EventEmitter {
129129

130130
const resolved = await Promise.all([this.client.send(new PutObjectCommand(params)), clientConfig?.endpoint?.()]);
131131
const putResult = resolved[0];
132-
let endpoint: Endpoint = resolved[1];
132+
let endpoint: Endpoint | undefined = resolved[1];
133133

134134
if (!endpoint) {
135135
endpoint = toEndpointV1(

Diff for: packages/middleware-endpoint/src/resolveEndpointConfig.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,17 @@ interface PreviouslyResolved<T extends EndpointParameters = EndpointParameters>
4747
*/
4848
export interface EndpointResolvedConfig<T extends EndpointParameters = EndpointParameters> {
4949
/**
50-
* Resolved value for input {@link EndpointsInputConfig.endpoint}
51-
* @deprecated Use {@link EndpointResolvedConfig.endpointProvider} instead
50+
* Custom endpoint provided by the user.
51+
* This is normalized to a single interface from the various acceptable types.
52+
* This field will be undefined if a custom endpoint is not provided.
53+
*
54+
* As of endpoints 2.0, this config method can not be used to resolve
55+
* the endpoint for a service and region.
56+
*
57+
* @see https://github.com/aws/aws-sdk-js-v3/issues/4122
58+
* @deprecated Use {@link EndpointResolvedConfig.endpointProvider} instead.
5259
*/
53-
endpoint: Provider<Endpoint>;
60+
endpoint?: Provider<Endpoint>;
5461

5562
endpointProvider: (params: T, context?: { logger?: Logger }) => EndpointV2;
5663

Diff for: packages/middleware-serde/src/serdePlugin.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MetadataBearer,
66
MiddlewareStack,
77
Pluggable,
8+
Provider,
89
RequestSerializer,
910
ResponseDeserializer,
1011
SerializeHandlerOptions,
@@ -30,22 +31,22 @@ export const serializerMiddlewareOption: SerializeHandlerOptions = {
3031

3132
// Type the modifies the EndpointBearer to make it compatible with Endpoints 2.0 change.
3233
// Must be removed after all clients has been onboard the Endpoints 2.0
33-
export type V1OrV2Endpoint<T extends EndpointBearer> = T & {
34+
export type V1OrV2Endpoint = {
35+
// for v2
3436
urlParser?: UrlParser;
37+
38+
// for v1
39+
endpoint?: Provider<Endpoint>;
3540
};
3641

37-
export function getSerdePlugin<
38-
InputType extends object,
39-
SerDeContext extends EndpointBearer,
40-
OutputType extends MetadataBearer
41-
>(
42-
config: V1OrV2Endpoint<SerDeContext>,
43-
serializer: RequestSerializer<any, SerDeContext>,
42+
export function getSerdePlugin<InputType extends object, SerDeContext, OutputType extends MetadataBearer>(
43+
config: V1OrV2Endpoint,
44+
serializer: RequestSerializer<any, SerDeContext & EndpointBearer>,
4445
deserializer: ResponseDeserializer<OutputType, any, SerDeContext>
4546
): Pluggable<InputType, OutputType> {
4647
return {
4748
applyToStack: (commandStack: MiddlewareStack<InputType, OutputType>) => {
48-
commandStack.add(deserializerMiddleware(config, deserializer), deserializerMiddlewareOption);
49+
commandStack.add(deserializerMiddleware(config as SerDeContext, deserializer), deserializerMiddlewareOption);
4950
commandStack.add(serializerMiddleware(config, serializer), serializerMiddlewareOption);
5051
},
5152
};

Diff for: packages/middleware-serde/src/serializerMiddleware.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,21 @@ import type { V1OrV2Endpoint } from "./serdePlugin";
1212

1313
export const serializerMiddleware =
1414
<Input extends object, Output extends object, RuntimeUtils extends EndpointBearer>(
15-
options: V1OrV2Endpoint<RuntimeUtils>,
15+
options: V1OrV2Endpoint,
1616
serializer: RequestSerializer<any, RuntimeUtils>
1717
): SerializeMiddleware<Input, Output> =>
1818
(next: SerializeHandler<Input, Output>, context: HandlerExecutionContext): SerializeHandler<Input, Output> =>
1919
async (args: SerializeHandlerArguments<Input>): Promise<SerializeHandlerOutput<Output>> => {
2020
const endpoint =
2121
context.endpointV2?.url && options.urlParser
2222
? async () => options.urlParser!(context.endpointV2!.url as URL)
23-
: options.endpoint;
23+
: options.endpoint!;
2424

2525
if (!endpoint) {
2626
throw new Error("No valid endpoint provider available.");
2727
}
2828

29-
const request = await serializer(args.input, { ...options, endpoint });
29+
const request = await serializer(args.input, { ...options, endpoint } as RuntimeUtils);
3030

3131
return next({
3232
...args,

Diff for: private/client-api-test/jest.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const base = require("../../jest.config.base.js");
2+
3+
module.exports = {
4+
...base,
5+
};

Diff for: private/client-api-test/package.json

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "@aws-sdk/client-api-test",
3+
"description": "Test suite for client interface stability",
4+
"version": "3.0.0",
5+
"scripts": {
6+
"build": "concurrently 'yarn:build:cjs' 'yarn:build:es' 'yarn:build:types'",
7+
"build:cjs": "tsc -p tsconfig.cjs.json",
8+
"build:docs": "typedoc",
9+
"build:es": "tsc -p tsconfig.es.json",
10+
"build:include:deps": "lerna run --scope $npm_package_name --include-dependencies build",
11+
"build:types": "tsc -p tsconfig.types.json",
12+
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
13+
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
14+
"test": "jest --coverage --passWithNoTests"
15+
},
16+
"main": "./dist-cjs/index.js",
17+
"types": "./dist-types/index.d.ts",
18+
"module": "./dist-es/index.js",
19+
"sideEffects": false,
20+
"dependencies": {
21+
"@aws-sdk/client-s3": "*",
22+
"tslib": "^2.3.1"
23+
},
24+
"devDependencies": {
25+
"@tsconfig/node14": "1.0.3",
26+
"@types/node": "^12.7.5",
27+
"concurrently": "7.0.0",
28+
"downlevel-dts": "0.10.1",
29+
"typedoc": "0.19.2",
30+
"typescript": "~4.6.2"
31+
},
32+
"overrides": {
33+
"typedoc": {
34+
"typescript": "~4.6.2"
35+
}
36+
},
37+
"engines": {
38+
"node": ">=14.0.0"
39+
},
40+
"typesVersions": {
41+
"<4.0": {
42+
"dist-types/*": [
43+
"dist-types/ts3.4/*"
44+
]
45+
}
46+
},
47+
"files": [
48+
"dist-*"
49+
],
50+
"author": {
51+
"name": "AWS SDK for JavaScript Team",
52+
"url": "https://aws.amazon.com/javascript/"
53+
},
54+
"license": "Apache-2.0",
55+
"private": true,
56+
"repository": {
57+
"type": "git",
58+
"url": "https://github.com/aws/aws-sdk-js-v3.git",
59+
"directory": "private/client-api-test"
60+
}
61+
}

Diff for: private/client-api-test/readme.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# @aws-sdk/client-api-test
2+
3+
This is not a runtime or published package.
4+
5+
This is a test spec.
6+
7+
The purpose of this package is to stabilize the `@aws-sdk/client-*` interface against changes.
8+
9+
If tests in this package fail, the author should either fix their changes such that the API contract
10+
is maintained, or appropriately announce and safely deprecate the interfaces affected by incoming changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Pattern for testing the stability of a Client interface.
3+
*/
4+
export interface ClientInterfaceTest<Client> {
5+
/**
6+
* Assert that some resolved config fields can be set to undefined.
7+
*/
8+
optionalConfigFieldsCanBeVoided(): void;
9+
/**
10+
* Create a test that initializes a client
11+
* with the minimal number of user-supplied values. This is
12+
* usually 0.
13+
*
14+
* This method is also a compilation test.
15+
*/
16+
initializeWithMinimalConfiguration(): Client;
17+
/**
18+
* Create a test that initializes a client with all config fields supplied
19+
* by the user.
20+
*
21+
* This method is also a compilation test.
22+
*/
23+
initializeWithMaximalConfiguration(): Client;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* The status of a config field after passing through constructor
3+
* resolvers.
4+
*/
5+
export type FIELD_INIT_TYPE = "resolvedByConfigResolver" | "resolvedOnlyIfProvided" | "neverResolved";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ClientS3InterfaceTest } from "./ClientS3InterfaceTest";
2+
import { RESOLVED_FIELDS } from "./RESOLVED_FIELDS";
3+
4+
const Subject = ClientS3InterfaceTest;
5+
6+
describe("Client config interface should be stable", () => {
7+
describe(ClientS3InterfaceTest.name, () => {
8+
describe("initialization with minimal configuration", () => {
9+
const client = new Subject().initializeWithMinimalConfiguration();
10+
for (const [configType, fields] of Object.entries(RESOLVED_FIELDS)) {
11+
for (const field of fields) {
12+
if (configType === "resolvedByConfigResolver") {
13+
it(`should resolve the field [${field}] after minimal client init`, () => {
14+
expect(client.config[field as keyof typeof client.config]).toBeDefined();
15+
});
16+
} else {
17+
it(`should not resolve the field [${field}] after minimal client init`, () => {
18+
expect(client.config[field as keyof typeof client.config]).not.toBeDefined();
19+
});
20+
}
21+
}
22+
}
23+
});
24+
describe("initialization with maximal configuration", () => {
25+
const client = new Subject().initializeWithMaximalConfiguration();
26+
for (const [configType, fields] of Object.entries(RESOLVED_FIELDS)) {
27+
for (const field of fields) {
28+
if (configType === "resolvedByConfigResolver" || configType === "resolvedOnlyIfProvided") {
29+
it(`should resolve the field [${field}] after maximally configured client init`, () => {
30+
expect(client.config[field as keyof typeof client.config]).toBeDefined();
31+
});
32+
} else {
33+
it(`should not resolve the field [${field}] after maximally configured client init`, () => {
34+
expect(client.config[field as keyof typeof client.config]).not.toBeDefined();
35+
});
36+
}
37+
}
38+
}
39+
});
40+
});
41+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { S3Client, S3ClientResolvedConfig } from "@aws-sdk/client-s3";
2+
3+
import { ClientInterfaceTest } from "../ClientInterfaceTest";
4+
import { initializeWithMaximalConfiguration } from "./impl/initializeWithMaximalConfiguration";
5+
import { initializeWithMinimalConfiguration } from "./impl/initializeWithMinimalConfiguration";
6+
7+
export class ClientS3InterfaceTest implements ClientInterfaceTest<S3Client> {
8+
optionalConfigFieldsCanBeVoided(): void {
9+
const s3 = new S3Client({});
10+
const resolvedConfig: S3ClientResolvedConfig = s3.config;
11+
/**
12+
* Endpoint is no longer guaranteed as of endpoints 2.0 (rulesets).
13+
* @see https://github.com/aws/aws-sdk-js-v3/issues/4122
14+
*/
15+
resolvedConfig.endpoint = void 0;
16+
resolvedConfig.isCustomEndpoint = void 0;
17+
resolvedConfig.customUserAgent = void 0;
18+
}
19+
initializeWithMinimalConfiguration(): S3Client {
20+
return initializeWithMinimalConfiguration();
21+
}
22+
initializeWithMaximalConfiguration(): S3Client {
23+
return initializeWithMaximalConfiguration();
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { S3ClientResolvedConfig } from "@aws-sdk/client-s3";
2+
3+
import { FIELD_INIT_TYPE } from "../FIELD_INIT_TYPE";
4+
5+
export const RESOLVED_FIELDS: Record<FIELD_INIT_TYPE, (keyof S3ClientResolvedConfig)[]> = {
6+
resolvedByConfigResolver: [
7+
"requestHandler",
8+
"apiVersion",
9+
"sha256",
10+
"urlParser",
11+
"bodyLengthChecker",
12+
"streamCollector",
13+
"base64Decoder",
14+
"base64Encoder",
15+
"utf8Decoder",
16+
"utf8Encoder",
17+
"runtime",
18+
"disableHostPrefix",
19+
"maxAttempts",
20+
"retryMode",
21+
"logger",
22+
"useDualstackEndpoint",
23+
"useFipsEndpoint",
24+
"serviceId",
25+
"region",
26+
"credentialDefaultProvider",
27+
"signingEscapePath",
28+
"useArnRegion",
29+
"defaultUserAgentProvider",
30+
"streamHasher",
31+
"md5",
32+
"sha1",
33+
"getAwsChunkedEncodingStream",
34+
"eventStreamSerdeProvider",
35+
"defaultsMode",
36+
"sdkStreamMixin",
37+
"endpointProvider",
38+
"tls",
39+
"isCustomEndpoint",
40+
"retryStrategy",
41+
"credentials",
42+
"signer",
43+
"systemClockOffset",
44+
"forcePathStyle",
45+
"useAccelerateEndpoint",
46+
"disableMultiregionAccessPoints",
47+
"eventStreamMarshaller",
48+
"defaultSigningName",
49+
"useGlobalEndpoint",
50+
],
51+
resolvedOnlyIfProvided: ["customUserAgent", "endpoint"],
52+
neverResolved: [],
53+
};

0 commit comments

Comments
 (0)