Skip to content

fix: sdk exploded syntax - take into account request plugin options #3076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/@ama-sdk/client-angular/src/api-angular-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ const DEFAULT_OPTIONS = {
angularPlugins: [],
requestPlugins: [],
enableTokenization: false,
disableFallback: false
disableFallback: false,
enableParameterSerialization: false
} as const satisfies Omit<BaseApiAngularClientOptions, 'basePath' | 'httpClient'>;

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

/** @inheritdoc */
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
return prepareUrlWithQueryParams(url, serializedQueryParams);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/@ama-sdk/client-beacon/src/api-beacon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export interface BaseApiBeaconClientConstructor extends PartialExcept<Omit<BaseA
const DEFAULT_OPTIONS = {
replyPlugins: [] as never[],
requestPlugins: [],
enableTokenization: false
enableTokenization: false,
enableParameterSerialization: false
} as const satisfies Omit<BaseApiBeaconClientOptions, 'basePath'>;

/**
Expand Down Expand Up @@ -130,7 +131,7 @@ export class ApiBeaconClient implements ApiClient {
}

/** @inheritdoc */
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
return prepareUrlWithQueryParams(url, serializedQueryParams);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/@ama-sdk/client-fetch/src/api-fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const DEFAULT_OPTIONS = {
fetchPlugins: [],
requestPlugins: [],
enableTokenization: false,
disableFallback: false
disableFallback: false,
enableParameterSerialization: false
} as const satisfies Omit<BaseApiFetchClientOptions, 'basePath'>;

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

/** @inheritdoc */
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
return prepareUrlWithQueryParams(url, serializedQueryParams);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/@ama-sdk/core/src/clients/api-angular-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ const DEFAULT_OPTIONS: Omit<BaseApiAngularClientOptions, 'basePath' | 'httpClien
angularPlugins: [],
requestPlugins: [],
enableTokenization: false,
disableFallback: false
disableFallback: false,
enableParameterSerialization: false
};

/**
Expand Down Expand Up @@ -160,7 +161,7 @@ export class ApiAngularClient implements ApiClient {
}

/** @inheritdoc */
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {
return prepareUrlWithQueryParams(url, serializedQueryParams);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/@ama-sdk/core/src/clients/api-beacon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
const DEFAULT_OPTIONS: Omit<BaseApiBeaconClientOptions, 'basePath'> = {
replyPlugins: [] as never[],
requestPlugins: [],
enableTokenization: false
enableTokenization: false,
enableParameterSerialization: false
};

/**
Expand Down Expand Up @@ -148,7 +149,7 @@
}

/** @inheritdoc */
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {

Check warning on line 152 in packages/@ama-sdk/core/src/clients/api-beacon-client.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L152 was not covered by tests
return prepareUrlWithQueryParams(url, serializedQueryParams);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/@ama-sdk/core/src/clients/api-fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
fetchPlugins: [],
requestPlugins: [],
enableTokenization: false,
disableFallback: false
disableFallback: false,
enableParameterSerialization: false
};

/**
Expand Down Expand Up @@ -156,7 +157,7 @@
}

/** @inheritdoc */
public prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
public prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string {

Check warning on line 160 in packages/@ama-sdk/core/src/clients/api-fetch-client.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L160 was not covered by tests
return prepareUrlWithQueryParams(url, serializedQueryParams);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/@ama-sdk/core/src/fwk/api.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
* Prepares the url to be called
* @param url Base url to be used
* @param queryParameters Key value pair with the parameters. If the value is undefined, the key is dropped
* @deprecated use {@link prepareUrlWithQueryParams} with query parameter serialization, will be removed in v14.
*/
export function prepareUrl(url: string, queryParameters: { [key: string]: string | undefined } = {}) {
const queryPart = Object.keys(queryParameters)
Expand All @@ -31,7 +30,7 @@ export function prepareUrl(url: string, queryParameters: { [key: string]: string
* @param url Base url to be used
* @param serializedQueryParams Key value pairs of query parameter names and their serialized values
*/
export function prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string {
export function prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string } = {}): string {
const paramsPrefix = url.includes('?') ? '&' : '?';
const queryPart = Object.values(serializedQueryParams).join('&');
return url + (queryPart ? paramsPrefix + queryPart : '');
Expand Down
5 changes: 4 additions & 1 deletion packages/@ama-sdk/core/src/fwk/core/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
ParamSerializationOptions,
RequestBody,
RequestMetadata,
RequestOptions,
Expand Down Expand Up @@ -27,6 +28,8 @@ export interface RequestOptionsParameters {
basePath: string;
/** Query Parameters */
queryParams?: { [key: string]: string | undefined };
/** Parameter serialization options */
paramSerializationOptions?: ParamSerializationOptions;
/** Force body to string */
body?: RequestBody;
/** Force headers to Headers type */
Expand Down Expand Up @@ -91,7 +94,7 @@ export interface ApiClient {
* @param url Base url to be used
* @param serializedQueryParams Key value pairs of query parameter names and their serialized values
*/
prepareUrlWithQueryParams(url: string, serializedQueryParams: { [key: string]: string }): string;
prepareUrlWithQueryParams(url: string, serializedQueryParams?: { [key: string]: string }): string;

/**
* Serialize query parameters based on the values of exploded and style
Expand Down
2 changes: 2 additions & 0 deletions packages/@ama-sdk/core/src/fwk/core/base-api-constructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface BaseApiClientOptions {
disableFallback?: boolean;
/** Logger (optional, fallback to console logger if undefined) */
logger?: Logger;
/** Enable parameter serialization with exploded syntax */
enableParameterSerialization?: boolean;
/** Custom query parameter serialization method */
serializeQueryParams?<T extends { [key: string]: SupportedParamType }>(queryParams: T, queryParamSerialization: { [p in keyof T]: ParamSerialization }): { [p in keyof T]: string };
/** Custom query parameter serialization method */
Expand Down
54 changes: 44 additions & 10 deletions packages/@ama-sdk/core/src/fwk/param-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ export type PrimitiveType = string | number | boolean | Date | utils.Date | util
/** Supported types for the operation parameters - primitives, primitive arrays, and simple non-nested objects */
export type SupportedParamType = PrimitiveType | PrimitiveType[] | { [key: string]: PrimitiveType };

/** URL encoding of space character, delimiter for spaceDelimited style */
const SPACE_URL_CODE = encodeURIComponent(' ');
/** URL encoding of pipe character, delimiter for pipeDelimited style */
const PIPE_URL_CODE = encodeURIComponent('|');
/** URL encoding of opening square bracket, used in deepObject style */
const OPENING_SQUARE_BRACKET_URL_CODE = encodeURIComponent('[');
/** URL encoding of closing square bracket, used in deepObject style */
const CLOSING_SQUARE_BRACKET_URL_CODE = encodeURIComponent(']');

/**
* Verify if property is of type utils.Date or utils.DateTime
* @param prop
Expand All @@ -23,6 +32,31 @@ export function isDateType(prop: any): prop is Date | utils.Date | utils.DateTim
return prop instanceof Date || prop instanceof utils.Date || prop instanceof utils.DateTime;
}

/**
* Check if the parameter is a record of type <string, string>.
* @param param
*/
export function isParamValueRecord(param: any): param is { [key: string]: string } {
return typeof param === 'object' && Object.values(param).every((item) => typeof item === 'string');
}

/** Query parameter value and serialization */
export type QueryParamValueSerialization = { value: SupportedParamType } & ParamSerialization;

/**
* Serialize query parameters of request plugins
* @param queryParams
*/
export function serializeRequestPluginQueryParams(queryParams: { [key: string]: QueryParamValueSerialization }) {
const queryParamsValues: { [key: string]: SupportedParamType } = {};
const queryParamSerialization: { [key: string]: ParamSerialization } = {};
Object.entries(queryParams).forEach(([paramKey, paramValue]) => {
queryParamsValues[paramKey] = paramValue.value;
queryParamSerialization[paramKey] = { explode: paramValue.explode, style: paramValue.style };
});
return serializeQueryParams(queryParamsValues, queryParamSerialization);
}

/**
* Serialize query parameters of type array
* OpenAPI Parameter Serialization {@link https://swagger.io/specification | documentation}
Expand All @@ -46,13 +80,13 @@ function serializeArrayQueryParams(queryParamName: string, queryParamValue: Prim
if (emptyArray) {
break;
}
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join('%20');
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join(SPACE_URL_CODE);
}
case 'pipeDelimited': {
if (emptyArray) {
break;
}
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join('%7C');
return encodeURIComponent(queryParamName) + '=' + filteredArray.map((v) => isDateType(v) ? v.toJSON() : encodeURIComponent(v.toString())).join(PIPE_URL_CODE);
}
}
}
Expand Down Expand Up @@ -81,15 +115,16 @@ function serializeObjectQueryParams(queryParamName: string, queryParamValue: { [
encodeURIComponent(propName) + ',' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))).join(',');
} else if (paramSerialization.style === 'spaceDelimited' && !paramSerialization.explode && !emptyObject) {
return encodeURIComponent(queryParamName) + '=' + Object.entries(filteredObject).map(([propName, propValue]) =>
encodeURIComponent(propName) + '%20' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
).join('%20');
encodeURIComponent(propName) + SPACE_URL_CODE + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
).join(SPACE_URL_CODE);
} else if (paramSerialization.style === 'pipeDelimited' && !paramSerialization.explode && !emptyObject) {
return encodeURIComponent(queryParamName) + '=' + Object.entries(filteredObject).map(([propName, propValue]) =>
encodeURIComponent(propName) + '%7C' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
).join('%7C');
encodeURIComponent(propName) + PIPE_URL_CODE + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
).join(PIPE_URL_CODE);
} else if (paramSerialization.style === 'deepObject' && paramSerialization.explode && !emptyObject) {
return Object.entries(filteredObject).map(([propName, propValue]) =>
encodeURIComponent(queryParamName) + '%5B' + encodeURIComponent(propName) + '%5D=' + (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
encodeURIComponent(queryParamName) + OPENING_SQUARE_BRACKET_URL_CODE + encodeURIComponent(propName) + CLOSING_SQUARE_BRACKET_URL_CODE + '='
+ (isDateType(propValue) ? propValue.toJSON() : encodeURIComponent(propValue.toString()))
).join('&');
}
}
Expand All @@ -110,9 +145,8 @@ export function serializeQueryParams<T extends { [key: string]: SupportedParamTy
} else if (typeof queryParamValue === 'object' && !isDateType(queryParamValue)) {
serializedValue = serializeObjectQueryParams(queryParamName, queryParamValue, paramSerialization);
} else {
if (paramSerialization.style === 'form') {
serializedValue = encodeURIComponent(queryParamName) + '=' + (isDateType(queryParamValue) ? queryParamValue.toJSON() : encodeURIComponent(queryParamValue.toString()));
}
// NOTE: 'form' style is the default value for primitive types
serializedValue = encodeURIComponent(queryParamName) + '=' + (isDateType(queryParamValue) ? queryParamValue.toJSON() : encodeURIComponent(queryParamValue.toString()));
}
if (serializedValue) {
acc[queryParamName as keyof T] = serializedValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import {
stringifyQueryParams,
} from '../../fwk/api.helpers';
import {
isParamValueRecord,
type QueryParamValueSerialization,
serializeRequestPluginQueryParams,
} from '../../fwk/param-serialization';
import {
PluginSyncRunner,
RequestOptions,
RequestPlugin,
RequestPluginContext,
} from '../core';
import {
isStringOrUndefined,
Expand All @@ -11,7 +20,8 @@ export interface AdditionalParametersSync {
/** Additional headers */
headers?: { [key: string]: string } | ((headers: Headers) => { [key: string]: string });
/** Additional query params */
queryParams?: { [key: string]: string } | ((defaultValues?: { [key: string]: string }) => { [key: string]: string });
queryParams?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }
| ((defaultValues?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }) => { [key: string]: string } | { [key: string]: QueryParamValueSerialization });
/** Additional body params */
body?: (defaultValues?: string) => string | null;
}
Expand All @@ -30,15 +40,29 @@ export class AdditionalParamsSyncRequest implements RequestPlugin {
this.additionalParams = additionalParams;
}

public load(): PluginSyncRunner<RequestOptions, RequestOptions> {
public load(context?: RequestPluginContext): PluginSyncRunner<RequestOptions, RequestOptions> {
return {
transform: (data: RequestOptions) => {
const queryParams = typeof this.additionalParams.queryParams === 'function' ? this.additionalParams.queryParams(data.queryParams) : this.additionalParams.queryParams;
const headers = typeof this.additionalParams.headers === 'function' ? this.additionalParams.headers(data.headers) : this.additionalParams.headers;
const body = this.additionalParams.body && isStringOrUndefined(data.body) ? this.additionalParams.body(data.body) : undefined;

if (queryParams) {
data.queryParams = { ...data.queryParams, ...queryParams };
if (data.paramSerializationOptions?.enableParameterSerialization) {
if (isParamValueRecord(queryParams)) {
throw new Error('It is not possible to serialize additional query parameters without their serialization properties `value`, `explode`, and `style`.');
} else {
data.queryParams = { ...data.queryParams, ...serializeRequestPluginQueryParams(queryParams) };
}
} else {
if (isParamValueRecord(queryParams)) {
data.queryParams = { ...data.queryParams, ...queryParams };
} else {
const queryParamsValues = Object.fromEntries(Object.entries(queryParams).map(([key, value]) => [key, value.value]));
data.queryParams = { ...data.queryParams, ...stringifyQueryParams(queryParamsValues) };
(context?.logger || console).log('The serialization of additional query parameters has been ignored since parameter serialization is not enabled.');
}
}
}

if (body !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import {
stringifyQueryParams,
} from '../../fwk/api.helpers';
import {
isParamValueRecord,
type QueryParamValueSerialization,
serializeRequestPluginQueryParams,
} from '../../fwk/param-serialization';
import {
PluginRunner,
RequestOptions,
RequestPlugin,
RequestPluginContext,
} from '../core';

export interface AdditionalParameters {
/** Additional headers */
headers?: { [key: string]: string } | ((headers: Headers) => { [key: string]: string } | Promise<{ [key: string]: string }>);
/** Additional query params */
queryParams?: { [key: string]: string } | ((defaultValues?: { [key: string]: string }) => { [key: string]: string } | Promise<{ [key: string]: string }>);
queryParams?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }
| ((defaultValues?: { [key: string]: string } | { [key: string]: QueryParamValueSerialization }) =>
{ [key: string]: string } | { [key: string]: QueryParamValueSerialization } | Promise<{ [key: string]: string }> | Promise<{ [key: string]: QueryParamValueSerialization }>
);
/** Additional body params */
body?: (defaultValues?: string) => string | null | Promise<string>;
}
Expand Down Expand Up @@ -37,15 +49,29 @@ export class AdditionalParamsRequest implements RequestPlugin {
this.additionalParams = additionalParams;
}

public load(): PluginRunner<RequestOptions, RequestOptions> {
public load(context?: RequestPluginContext): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: async (data: RequestOptions) => {
const queryParams = typeof this.additionalParams.queryParams === 'function' ? await this.additionalParams.queryParams(data.queryParams) : this.additionalParams.queryParams;
const headers = typeof this.additionalParams.headers === 'function' ? await this.additionalParams.headers(data.headers) : this.additionalParams.headers;
const body = this.additionalParams.body && isStringOrUndefined(data.body) ? this.additionalParams.body(data.body) : undefined;

if (queryParams) {
data.queryParams = { ...data.queryParams, ...queryParams };
if (data.paramSerializationOptions?.enableParameterSerialization) {
if (isParamValueRecord(queryParams)) {
throw new Error('It is not possible to serialize additional query parameters without their serialization properties `value`, `explode`, and `style`.');
} else {
data.queryParams = { ...data.queryParams, ...serializeRequestPluginQueryParams(queryParams) };
}
} else {
if (isParamValueRecord(queryParams)) {
data.queryParams = { ...data.queryParams, ...queryParams };
} else {
const queryParamsValues = Object.fromEntries(Object.entries(queryParams).map(([key, value]) => [key, value.value]));
data.queryParams = { ...data.queryParams, ...stringifyQueryParams(queryParamsValues) };
(context?.logger || console).log('The serialization of additional query parameters has been ignored since parameter serialization is not enabled.');
}
}
}

if (body !== undefined) {
Expand Down
Loading
Loading