Skip to content

Commit 5a3d24b

Browse files
committed
fix: sdk exploded syntax - take into account request plugin options and
make serialization optional
1 parent c894748 commit 5a3d24b

File tree

23 files changed

+609
-153
lines changed

23 files changed

+609
-153
lines changed

packages/@ama-sdk/client-angular/src/api-angular-client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ const DEFAULT_OPTIONS = {
5555
angularPlugins: [],
5656
requestPlugins: [],
5757
enableTokenization: false,
58-
disableFallback: false
58+
disableFallback: false,
59+
enableParameterSerialization: false
5960
} as const satisfies Omit<BaseApiAngularClientOptions, 'basePath' | 'httpClient'>;
6061

6162
/** Client to process the call to the API using Angular API */
@@ -130,7 +131,7 @@ export class ApiAngularClient implements ApiClient {
130131
}
131132

132133
/** @inheritdoc */
133-
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
134+
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
134135
return prepareUrlWithQueryParams(url, serializedQueryParams);
135136
}
136137

packages/@ama-sdk/client-beacon/src/api-beacon-client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export interface BaseApiBeaconClientConstructor extends PartialExcept<Omit<BaseA
3535
const DEFAULT_OPTIONS = {
3636
replyPlugins: [] as never[],
3737
requestPlugins: [],
38-
enableTokenization: false
38+
enableTokenization: false,
39+
enableParameterSerialization: false
3940
} as const satisfies Omit<BaseApiBeaconClientOptions, 'basePath'>;
4041

4142
/**
@@ -130,7 +131,7 @@ export class ApiBeaconClient implements ApiClient {
130131
}
131132

132133
/** @inheritdoc */
133-
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
134+
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
134135
return prepareUrlWithQueryParams(url, serializedQueryParams);
135136
}
136137

packages/@ama-sdk/client-fetch/src/api-fetch-client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ const DEFAULT_OPTIONS = {
5151
fetchPlugins: [],
5252
requestPlugins: [],
5353
enableTokenization: false,
54-
disableFallback: false
54+
disableFallback: false,
55+
enableParameterSerialization: false
5556
} as const satisfies Omit<BaseApiFetchClientOptions, 'basePath'>;
5657

5758
/** Client to process the call to the API using Fetch API */
@@ -131,7 +132,7 @@ export class ApiFetchClient implements ApiClient {
131132
}
132133

133134
/** @inheritdoc */
134-
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
135+
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
135136
return prepareUrlWithQueryParams(url, serializedQueryParams);
136137
}
137138

packages/@ama-sdk/core/src/clients/api-angular-client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ const DEFAULT_OPTIONS: Omit<BaseApiAngularClientOptions, 'basePath' | 'httpClien
8282
angularPlugins: [],
8383
requestPlugins: [],
8484
enableTokenization: false,
85-
disableFallback: false
85+
disableFallback: false,
86+
enableParameterSerialization: false
8687
};
8788

8889
/**
@@ -160,7 +161,7 @@ export class ApiAngularClient implements ApiClient {
160161
}
161162

162163
/** @inheritdoc */
163-
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
164+
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
164165
return prepareUrlWithQueryParams(url, serializedQueryParams);
165166
}
166167

packages/@ama-sdk/core/src/clients/api-beacon-client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export interface BaseApiBeaconClientConstructor extends PartialExcept<Omit<BaseA
5252
const DEFAULT_OPTIONS: Omit<BaseApiBeaconClientOptions, 'basePath'> = {
5353
replyPlugins: [] as never[],
5454
requestPlugins: [],
55-
enableTokenization: false
55+
enableTokenization: false,
56+
enableParameterSerialization: false
5657
};
5758

5859
/**
@@ -148,7 +149,7 @@ export class ApiBeaconClient implements ApiClient {
148149
}
149150

150151
/** @inheritdoc */
151-
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
152+
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
152153
return prepareUrlWithQueryParams(url, serializedQueryParams);
153154
}
154155

packages/@ama-sdk/core/src/clients/api-fetch-client.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ const DEFAULT_OPTIONS: Omit<BaseApiFetchClientOptions, 'basePath'> = {
7373
fetchPlugins: [],
7474
requestPlugins: [],
7575
enableTokenization: false,
76-
disableFallback: false
76+
disableFallback: false,
77+
enableParameterSerialization: false
7778
};
7879

7980
/**
@@ -156,7 +157,7 @@ export class ApiFetchClient implements ApiClient {
156157
}
157158

158159
/** @inheritdoc */
159-
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
160+
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
160161
return prepareUrlWithQueryParams(url, serializedQueryParams);
161162
}
162163

packages/@ama-sdk/core/src/fwk/api.helpers.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {
1313
* Prepares the url to be called
1414
* @param url Base url to be used
1515
* @param queryParameters Key value pair with the parameters. If the value is undefined, the key is dropped
16-
* @deprecated use {@link prepareUrlWithQueryParams} with query parameter serialization, will be removed in v14.
1716
*/
1817
export function prepareUrl(url: string, queryParameters: { [key: string]: string | undefined } = {}) {
1918
const queryPart = Object.keys(queryParameters)
@@ -31,7 +30,7 @@ export function prepareUrl(url: string, queryParameters: { [key: string]: string
3130
* @param url Base url to be used
3231
* @param serializedQueryParams Key value pairs of query parameter names and their serialized values
3332
*/
34-
export function prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
33+
export function prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string } = {}): string {
3534
const paramsPrefix = url.includes('?') ? '&' : '?';
3635
const queryPart = Object.values(serializedQueryParams).join('&');
3736
return url + (queryPart ? paramsPrefix + queryPart : '');

packages/@ama-sdk/core/src/fwk/core/api-client.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
ParamSerializationOptions,
23
RequestBody,
34
RequestMetadata,
45
RequestOptions,
@@ -27,6 +28,8 @@ export interface RequestOptionsParameters {
2728
basePath: string;
2829
/** Query Parameters */
2930
queryParams?: { [key: string]: string | undefined };
31+
/** Parameter serialization options */
32+
paramSerializationOptions?: ParamSerializationOptions;
3033
/** Force body to string */
3134
body?: RequestBody;
3235
/** Force headers to Headers type */
@@ -91,7 +94,7 @@ export interface ApiClient {
9194
* @param url Base url to be used
9295
* @param serializedQueryParams Key value pairs of query parameter names and their serialized values
9396
*/
94-
prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string;
97+
prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string;
9598

9699
/**
97100
* Serialize query parameters based on the values of exploded and style

packages/@ama-sdk/core/src/fwk/core/base-api-constructor.ts

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface BaseApiClientOptions {
2727
disableFallback?: boolean;
2828
/** Logger (optional, fallback to console logger if undefined) */
2929
logger?: Logger;
30+
/** Enable parameter serialization with exploded syntax */
31+
enableParameterSerialization?: boolean;
3032
/** Custom query parameter serialization method */
3133
serializeQueryParams?<T extends { [key: string]: SupportedParamType }>(queryParams: T, queryParamSerialization: { [p in keyof T]: ParamSerialization }): { [p in keyof T]: string };
3234
/** Custom query parameter serialization method */

packages/@ama-sdk/core/src/fwk/param-serialization.ts

+44-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ export type PrimitiveType = string | number | boolean | Date | utils.Date | util
1515
/** Supported types for the operation parameters - primitives, primitive arrays, and simple non-nested objects */
1616
export type SupportedParamType = PrimitiveType | PrimitiveType[] | { [key: string]: PrimitiveType };
1717

18+
/** URL encoding of space character, delimiter for spaceDelimited style */
19+
const SPACE_URL_CODE = encodeURIComponent(' ');
20+
/** URL encoding of pipe character, delimiter for pipeDelimited style */
21+
const PIPE_URL_CODE = encodeURIComponent('|');
22+
/** URL encoding of opening square bracket, used in deepObject style */
23+
const OPENING_SQUARE_BRACKET_URL_CODE = encodeURIComponent('[');
24+
/** URL encoding of closing square bracket, used in deepObject style */
25+
const CLOSING_SQUARE_BRACKET_URL_CODE = encodeURIComponent(']');
26+
1827
/**
1928
* Verify if property is of type utils.Date or utils.DateTime
2029
* @param prop
@@ -23,6 +32,31 @@ export function isDateType(prop: any): prop is Date | utils.Date | utils.DateTim
2332
return prop instanceof Date || prop instanceof utils.Date || prop instanceof utils.DateTime;
2433
}
2534

35+
/**
36+
* Check if the parameter is a record of type <string, string>.
37+
* @param param
38+
*/
39+
export function isParamValueRecord(param: any): param is { [key: string]: string } {
40+
return typeof param === 'object' && Object.values(param).every((item) => typeof item === 'string');
41+
}
42+
43+
/** Query parameter value and serialization */
44+
export type QueryParamValueSerialization = { value: SupportedParamType } & ParamSerialization;
45+
46+
/**
47+
* Serialize query parameters of request plugins
48+
* @param queryParams
49+
*/
50+
export function serializeRequestPluginQueryParams(queryParams: { [key: string]: QueryParamValueSerialization }) {
51+
const queryParamsValues: { [key: string]: SupportedParamType } = {};
52+
const queryParamSerialization: { [key: string]: ParamSerialization } = {};
53+
Object.entries(queryParams).forEach(([paramKey, paramValue]) => {
54+
queryParamsValues[paramKey] = paramValue.value;
55+
queryParamSerialization[paramKey] = { explode: paramValue.explode, style: paramValue.style };
56+
});
57+
return serializeQueryParams(queryParamsValues, queryParamSerialization);
58+
}
59+
2660
/**
2761
* Serialize query parameters of type array
2862
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
@@ -46,13 +80,13 @@ function serializeArrayQueryParams(queryParamName: string, queryParamValue: Prim
4680
if (emptyArray) {
4781
break;
4882
}
49-
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join('%20');
83+
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join(SPACE_URL_CODE);
5084
}
5185
case 'pipeDelimited': {
5286
if (emptyArray) {
5387
break;
5488
}
55-
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join('%7C');
89+
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join(PIPE_URL_CODE);
5690
}
5791
}
5892
}
@@ -81,15 +115,16 @@ function serializeObjectQueryParams(queryParamName: string, queryParamValue: { [
81115
encodeURIComponent(propName) + ',' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))).join(',');
82116
} else if (paramSerialization.style === 'spaceDelimited' && !paramSerialization.explode && !emptyObject) {
83117
return encodeURIComponent(queryParamName) + '=' + Object.entries(filteredObject).map(([propName, propValue]) =>
84-
encodeURIComponent(propName) + '%20' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
85-
).join('%20');
118+
encodeURIComponent(propName) + SPACE_URL_CODE + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
119+
).join(SPACE_URL_CODE);
86120
} else if (paramSerialization.style === 'pipeDelimited' && !paramSerialization.explode && !emptyObject) {
87121
return encodeURIComponent(queryParamName) + '=' + Object.entries(filteredObject).map(([propName, propValue]) =>
88-
encodeURIComponent(propName) + '%7C' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
89-
).join('%7C');
122+
encodeURIComponent(propName) + PIPE_URL_CODE + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
123+
).join(PIPE_URL_CODE);
90124
} else if (paramSerialization.style === 'deepObject' && paramSerialization.explode && !emptyObject) {
91125
return Object.entries(filteredObject).map(([propName, propValue]) =>
92-
encodeURIComponent(queryParamName) + '%5B' + encodeURIComponent(propName) + '%5D=' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
126+
encodeURIComponent(queryParamName) + OPENING_SQUARE_BRACKET_URL_CODE + encodeURIComponent(propName) + CLOSING_SQUARE_BRACKET_URL_CODE + '='
127+
+ (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
93128
).join('&');
94129
}
95130
}
@@ -110,9 +145,8 @@ export function serializeQueryParams<T extends { [key: string]: SupportedParamTy
110145
} else if (typeof queryParamValue === 'object' && !isDateType(queryParamValue)) {
111146
serializedValue = serializeObjectQueryParams(queryParamName, queryParamValue, paramSerialization);
112147
} else {
113-
if (paramSerialization.style === 'form') {
114-
serializedValue = encodeURIComponent(queryParamName) + '=' + (isDateType(queryParamValue) ? queryParamValue.toJSON() : encodeURIComponent(queryParamValue.toString()));
115-
}
148+
// NOTE: 'form' style is the default value for primitive types
149+
serializedValue = encodeURIComponent(queryParamName) + '=' + (isDateType(queryParamValue) ? queryParamValue.toJSON() : encodeURIComponent(queryParamValue.toString()));
116150
}
117151
if (serializedValue) {
118152
acc[queryParamName as keyof T] = serializedValue;

packages/@ama-sdk/core/src/plugins/additional-params/additional-params-sync.request.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import {
2+
stringifyQueryParams,
3+
} from '../../fwk/api.helpers';
4+
import {
5+
isParamValueRecord,
6+
type QueryParamValueSerialization,
7+
serializeRequestPluginQueryParams,
8+
} from '../../fwk/param-serialization';
19
import {
210
PluginSyncRunner,
311
RequestOptions,
412
RequestPlugin,
13+
RequestPluginContext,
514
} from '../core';
615
import {
716
isStringOrUndefined,
@@ -11,7 +20,8 @@ export interface AdditionalParametersSync {
1120
/** Additional headers */
1221
headers?: { [key: string]: string } | ((headers: Headers) => { [key: string]: string });
1322
/** Additional query params */
14-
queryParams?: { [key: string]: string } | ((defaultValues?: { [key: string]: string }) => { [key: string]: string });
23+
queryParams?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }
24+
| ((defaultValues?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }) => { [key: string]: string } | { [key: string]: QueryParamValueSerialization });
1525
/** Additional body params */
1626
body?: (defaultValues?: string) => string | null;
1727
}
@@ -30,15 +40,29 @@ export class AdditionalParamsSyncRequest implements RequestPlugin {
3040
this.additionalParams = additionalParams;
3141
}
3242

33-
public load(): PluginSyncRunner<RequestOptions, RequestOptions> {
43+
public load(context?: RequestPluginContext): PluginSyncRunner<RequestOptions, RequestOptions> {
3444
return {
3545
transform: (data: RequestOptions) => {
3646
const queryParams = typeof this.additionalParams.queryParams === 'function' ? this.additionalParams.queryParams(data.queryParams) : this.additionalParams.queryParams;
3747
const headers = typeof this.additionalParams.headers === 'function' ? this.additionalParams.headers(data.headers) : this.additionalParams.headers;
3848
const body = this.additionalParams.body && isStringOrUndefined(data.body) ? this.additionalParams.body(data.body) : undefined;
3949

4050
if (queryParams) {
41-
data.queryParams = { ...data.queryParams, ...queryParams };
51+
if (data.paramSerializationOptions?.enableParameterSerialization) {
52+
if (isParamValueRecord(queryParams)) {
53+
throw new Error('It is not possible to serialize additional query parameters without their serialization properties `value`, `explode`, and `style`.');
54+
} else {
55+
data.queryParams = { ...data.queryParams, ...serializeRequestPluginQueryParams(queryParams) };
56+
}
57+
} else {
58+
if (isParamValueRecord(queryParams)) {
59+
data.queryParams = { ...data.queryParams, ...queryParams };
60+
} else {
61+
const queryParamsValues = Object.fromEntries(Object.entries(queryParams).map(([key, value]) => [key, value.value]));
62+
data.queryParams = { ...data.queryParams, ...stringifyQueryParams(queryParamsValues) };
63+
(context?.logger || console).log('The serialization of additional query parameters has been ignored since parameter serialization is not enabled.');
64+
}
65+
}
4266
}
4367

4468
if (body !== undefined) {

packages/@ama-sdk/core/src/plugins/additional-params/additional-params.request.ts

+29-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1+
import {
2+
stringifyQueryParams,
3+
} from '../../fwk/api.helpers';
4+
import {
5+
isParamValueRecord,
6+
type QueryParamValueSerialization,
7+
serializeRequestPluginQueryParams,
8+
} from '../../fwk/param-serialization';
19
import {
210
PluginRunner,
311
RequestOptions,
412
RequestPlugin,
13+
RequestPluginContext,
514
} from '../core';
615

716
export interface AdditionalParameters {
817
/** Additional headers */
918
headers?: { [key: string]: string } | ((headers: Headers) => { [key: string]: string } | Promise<{ [key: string]: string }>);
1019
/** Additional query params */
11-
queryParams?: { [key: string]: string } | ((defaultValues?: { [key: string]: string }) => { [key: string]: string } | Promise<{ [key: string]: string }>);
20+
queryParams?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }
21+
| ((defaultValues?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }) =>
22+
{ [key: string]: string } | { [key: string]: QueryParamValueSerialization } | Promise<{ [key: string]: string }> | Promise<{ [key: string]: QueryParamValueSerialization }>
23+
);
1224
/** Additional body params */
1325
body?: (defaultValues?: string) => string | null | Promise<string>;
1426
}
@@ -37,15 +49,29 @@ export class AdditionalParamsRequest implements RequestPlugin {
3749
this.additionalParams = additionalParams;
3850
}
3951

40-
public load(): PluginRunner<RequestOptions, RequestOptions> {
52+
public load(context?: RequestPluginContext): PluginRunner<RequestOptions, RequestOptions> {
4153
return {
4254
transform: async (data: RequestOptions) => {
4355
const queryParams = typeof this.additionalParams.queryParams === 'function' ? await this.additionalParams.queryParams(data.queryParams) : this.additionalParams.queryParams;
4456
const headers = typeof this.additionalParams.headers === 'function' ? await this.additionalParams.headers(data.headers) : this.additionalParams.headers;
4557
const body = this.additionalParams.body && isStringOrUndefined(data.body) ? this.additionalParams.body(data.body) : undefined;
4658

4759
if (queryParams) {
48-
data.queryParams = { ...data.queryParams, ...queryParams };
60+
if (data.paramSerializationOptions?.enableParameterSerialization) {
61+
if (isParamValueRecord(queryParams)) {
62+
throw new Error('It is not possible to serialize additional query parameters without their serialization properties `value`, `explode`, and `style`.');
63+
} else {
64+
data.queryParams = { ...data.queryParams, ...serializeRequestPluginQueryParams(queryParams) };
65+
}
66+
} else {
67+
if (isParamValueRecord(queryParams)) {
68+
data.queryParams = { ...data.queryParams, ...queryParams };
69+
} else {
70+
const queryParamsValues = Object.fromEntries(Object.entries(queryParams).map(([key, value]) => [key, value.value]));
71+
data.queryParams = { ...data.queryParams, ...stringifyQueryParams(queryParamsValues) };
72+
(context?.logger || console).log('The serialization of additional query parameters has been ignored since parameter serialization is not enabled.');
73+
}
74+
}
4975
}
5076

5177
if (body !== undefined) {

0 commit comments

Comments
 (0)