Skip to content

Commit

Permalink
feat(sdk): provide a way to request a custom URL and add apiName in r…
Browse files Browse the repository at this point in the history
…equest plugin context (#1578)

## Proposed change

feat(sdk): provide a way to request a custom URL
feat(sdk): add apiName in the request plugin context

The purpose of this PR is to offer a way to do manually a request in the
context of the SDK and benefit of the plugins register.
To facilitate the identification of the request in the requestPlugin (as
done in the replyPlugin), the context is provided to the request plugin.

<!-- Please include a summary of the changes and the related issue.
Please also include relevant motivation and context. List any
dependencies that is required for this change. -->
  • Loading branch information
kpanot authored Apr 23, 2024
2 parents 33f2911 + b5c1252 commit 1d23e04
Show file tree
Hide file tree
Showing 19 changed files with 364 additions and 355 deletions.
43 changes: 28 additions & 15 deletions packages/@ama-sdk/core/src/clients/api-angular-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ExceptionReply } from '../plugins/exception';
import { ReviverReply } from '../plugins/reviver';
import { ApiTypes } from '../fwk/api';
import { extractQueryParams, filterUndefinedValues, getResponseReviver, prepareUrl, processFormData, tokenizeRequestOptions } from '../fwk/api.helpers';
import type { PartialExcept } from '../fwk/api.interface';
import { ApiClient } from '../fwk/core/api-client';
import type { Api, PartialExcept } from '../fwk/api.interface';
import type { ApiClient,RequestOptionsParameters } from '../fwk/core/api-client';
import { BaseApiClientOptions } from '../fwk/core/base-api-constructor';
import { EmptyResponseError } from '../fwk/errors';
import { ReviverType } from '../fwk/Reviver';
Expand Down Expand Up @@ -48,28 +48,41 @@ export class ApiAngularClient implements ApiClient {
public extractQueryParams<T extends { [key: string]: any }>(data: T, names: (keyof T)[]): { [p in keyof T]: string; } {
return extractQueryParams(data, names);
}
public async prepareOptions(url: string, method: string, queryParams: { [key: string]: string | undefined }, headers: { [key: string]: string | undefined }, body?: RequestBody | undefined,
tokenizedOptions?: TokenizedOptions | undefined, metadata?: RequestMetadata<string, string> | undefined): Promise<RequestOptions> {
const options: RequestOptions = {
method,
headers: new Headers(filterUndefinedValues(headers)),
body,
queryParams: filterUndefinedValues(queryParams),
basePath: url,
tokenizedOptions,
metadata
};

let opts = options;
/** @inheritdoc */
public async getRequestOptions(requestOptionsParameters: RequestOptionsParameters): Promise<RequestOptions> {
let opts: RequestOptions = {
...requestOptionsParameters,
headers: new Headers(filterUndefinedValues(requestOptionsParameters.headers)),
queryParams: filterUndefinedValues(requestOptionsParameters.queryParams)
};
if (this.options.requestPlugins) {
for (const plugin of this.options.requestPlugins) {
opts = await plugin.load({logger: this.options.logger}).transform(opts);
opts = await plugin.load({
logger: this.options.logger,
apiName: requestOptionsParameters.api?.apiName
}).transform(opts);
}
}

return opts;
}

/** @inheritdoc */
public async prepareOptions(url: string, method: string, queryParams: { [key: string]: string | undefined }, headers: { [key: string]: string | undefined }, body?: RequestBody,
tokenizedOptions?: TokenizedOptions, metadata?: RequestMetadata, api?: Api) {
return this.getRequestOptions({
headers,
method,
basePath: url,
queryParams,
body,
metadata,
tokenizedOptions,
api
});
}

/** @inheritdoc */
public prepareUrl(url: string, queryParameters: { [key: string]: string | undefined } = {}) {
return prepareUrl(url, queryParameters);
Expand Down
44 changes: 26 additions & 18 deletions packages/@ama-sdk/core/src/clients/api-beacon-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { RequestBody, RequestMetadata, RequestOptions, TokenizedOptions } from '../plugins';
import type { ApiTypes } from '../fwk/api';
import { extractQueryParams, filterUndefinedValues, prepareUrl, processFormData, tokenizeRequestOptions } from '../fwk/api.helpers';
import type { PartialExcept } from '../fwk/api.interface';
import type { ApiClient } from '../fwk/core/api-client';
import type { Api, PartialExcept } from '../fwk/api.interface';
import type { ApiClient, RequestOptionsParameters } from '../fwk/core/api-client';
import type { BaseApiClientOptions } from '../fwk/core/base-api-constructor';

/** @see BaseApiClientOptions */
Expand All @@ -27,7 +27,7 @@ const DEFAULT_OPTIONS: Omit<BaseApiBeaconClientOptions, 'basePath'> = {
*/
// NOTE: the `extends unknown` is required for ESM build with TSC
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
const isPromise = <T extends unknown>(value: T | Promise<T>): value is Promise<T> => value && typeof (value as any).then === 'function';
const isPromise = <T extends unknown>(value: T | Promise<T>): value is Promise<T> => value instanceof Promise;

/**
* The Beacon API client is an implementation of the API Client using the Navigator Beacon API.
Expand Down Expand Up @@ -60,27 +60,20 @@ export class ApiBeaconClient implements ApiClient {
}

/** @inheritdoc */
public prepareOptions(url: string, method: string, queryParams: { [key: string]: string }, headers: { [key: string]: string }, body?: RequestBody,
tokenizedOptions?: TokenizedOptions, metadata?: RequestMetadata): Promise<RequestOptions> {
public getRequestOptions(options: RequestOptionsParameters): Promise<RequestOptions> {

if (method.toUpperCase() !== 'POST') {
throw new Error(`Unsupported method: ${method}. The beacon API only supports POST.`);
if (options.method.toUpperCase() !== 'POST') {
throw new Error(`Unsupported method: ${options.method}. The beacon API only supports POST.`);
}

const options: RequestOptions = {
method,
headers: new Headers(filterUndefinedValues(headers)),
body,
queryParams: filterUndefinedValues(queryParams),
basePath: url,
tokenizedOptions,
metadata
let opts: RequestOptions = {
...options,
headers: new Headers(filterUndefinedValues(options.headers)),
queryParams: filterUndefinedValues(options.queryParams)
};

let opts = options;
if (this.options.requestPlugins) {
for (const plugin of this.options.requestPlugins) {
const changedOpt = plugin.load({logger: this.options.logger}).transform(opts);
const changedOpt = plugin.load({ logger: this.options.logger, apiName: options.api?.apiName }).transform(opts);
if (isPromise(changedOpt)) {
throw new Error(`Request plugin ${plugin.constructor.name} has async transform method. Only sync methods are supported with the Beacon client.`);
} else {
Expand All @@ -92,6 +85,21 @@ export class ApiBeaconClient implements ApiClient {
return Promise.resolve(opts);
}

/** @inheritdoc */
public prepareOptions(url: string, method: string, queryParams: { [key: string]: string | undefined }, headers: { [key: string]: string | undefined }, body?: RequestBody,
tokenizedOptions?: TokenizedOptions, metadata?: RequestMetadata, api?: Api) {
return this.getRequestOptions({
headers,
method,
basePath: url,
queryParams,
body,
metadata,
tokenizedOptions,
api
});
}

/** @inheritdoc */
public prepareUrl(url: string, queryParameters?: { [key: string]: string }): string {
return prepareUrl(url, queryParameters);
Expand Down
41 changes: 26 additions & 15 deletions packages/@ama-sdk/core/src/clients/api-fetch-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {ExceptionReply} from '../plugins/exception';
import {ReviverReply} from '../plugins/reviver';
import {ApiTypes} from '../fwk/api';
import {extractQueryParams, filterUndefinedValues, getResponseReviver, prepareUrl, processFormData, tokenizeRequestOptions} from '../fwk/api.helpers';
import type {PartialExcept} from '../fwk/api.interface';
import {ApiClient} from '../fwk/core/api-client';
import type {Api, PartialExcept} from '../fwk/api.interface';
import type {ApiClient, RequestOptionsParameters} from '../fwk/core/api-client';
import {BaseApiClientOptions} from '../fwk/core/base-api-constructor';
import {CanceledCallError, EmptyResponseError, ResponseJSONParseError} from '../fwk/errors';
import {ReviverType} from '../fwk/Reviver';
Expand Down Expand Up @@ -64,28 +64,39 @@ export class ApiFetchClient implements ApiClient {
}

/** @inheritdoc */
public async prepareOptions(url: string, method: string, queryParams: { [key: string]: string | undefined }, headers: { [key: string]: string | undefined }, body?: RequestBody,
tokenizedOptions?: TokenizedOptions, metadata?: RequestMetadata) {
const options: RequestOptions = {
method,
headers: new Headers(filterUndefinedValues(headers)),
body,
queryParams: filterUndefinedValues(queryParams),
basePath: url,
tokenizedOptions,
metadata
public async getRequestOptions(requestOptionsParameters: RequestOptionsParameters): Promise<RequestOptions> {
let opts: RequestOptions = {
...requestOptionsParameters,
headers: new Headers(filterUndefinedValues(requestOptionsParameters.headers)),
queryParams: filterUndefinedValues(requestOptionsParameters.queryParams)
};

let opts = options;
if (this.options.requestPlugins) {
for (const plugin of this.options.requestPlugins) {
opts = await plugin.load({logger: this.options.logger}).transform(opts);
opts = await plugin.load({
logger: this.options.logger,
apiName: requestOptionsParameters.api?.apiName
}).transform(opts);
}
}

return opts;
}

/** @inheritdoc */
public async prepareOptions(url: string, method: string, queryParams: { [key: string]: string | undefined }, headers: { [key: string]: string | undefined }, body?: RequestBody,
tokenizedOptions?: TokenizedOptions, metadata?: RequestMetadata, api?: Api) {
return this.getRequestOptions({
headers,
method,
basePath: url,
queryParams,
body,
metadata,
tokenizedOptions,
api
});
}

/** @inheritdoc */
public prepareUrl(url: string, queryParameters: { [key: string]: string | undefined } = {}) {
return prepareUrl(url, queryParameters);
Expand Down
4 changes: 2 additions & 2 deletions packages/@ama-sdk/core/src/fwk/api.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function extractQueryParams<T extends { [key: string]: any }>(data: T, na
* @param object JSON object to filter
* @returns an object without undefined values
*/
export function filterUndefinedValues(object: { [key: string]: string | undefined }): { [key: string]: string } {
return Object.keys(object)
export function filterUndefinedValues(object?: { [key: string]: string | undefined }): { [key: string]: string } {
return !object ? {} : Object.keys(object)
.filter((objectKey) => typeof object[objectKey] !== 'undefined')
.reduce<{ [key: string]: string }>((acc, objectKey) => {
acc[objectKey] = object[objectKey] as string;
Expand Down
35 changes: 34 additions & 1 deletion packages/@ama-sdk/core/src/fwk/core/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import {RequestBody, RequestMetadata, RequestOptions, TokenizedOptions} from '../../plugins/index';
import {ApiTypes} from '../api';
import type { Api } from '../api.interface';
import {ReviverType} from '../Reviver';
import {BaseApiClientOptions} from './base-api-constructor';

/** Parameters to the request the call options */
export interface RequestOptionsParameters {
/** URL of the call to process (without the query parameters) */
basePath: string;
/** Query Parameters */
queryParams?: { [key: string]: string | undefined };
/** Force body to string */
body?: RequestBody;
/** Force headers to Headers type */
headers: { [key: string]: string | undefined };
/** Tokenized options to replace URL and query parameters */
tokenizedOptions?: TokenizedOptions;
/** Request metadata */
metadata?: RequestMetadata;
/** HTTP Method used for the request */
method: NonNullable<RequestInit['method']>;
/**
* API initializing the call
* @todo this field will be turned as mandatory in v11
*/
api?: Api;
}

/**
* API Client used by the SDK's APIs to call the server
*/
Expand All @@ -18,10 +42,19 @@ export interface ApiClient {
*/
extractQueryParams<T extends { [key: string]: any }>(data: T, names: (keyof T)[]): { [p in keyof T]: string; };

/** Prepare Options */
/**
* Prepare Options
* @deprecated use getRequestOptions instead, will be removed in v11
*/
prepareOptions(url: string, method: string, queryParams: { [key: string]: string | undefined }, headers: { [key: string]: string | undefined }, body?: RequestBody,
tokenizedOptions?: TokenizedOptions, metadata?: RequestMetadata): Promise<RequestOptions>;

/**
* Retrieve the option to process the HTTP Call
* @todo turn this function mandatory when `prepareOptions` will be removed
*/
getRequestOptions?(requestOptionsParameters: RequestOptionsParameters): Promise<RequestOptions>;

/**
* prepares the url to be called
* @param url base url to be used
Expand Down
7 changes: 6 additions & 1 deletion packages/@ama-sdk/core/src/plugins/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ export interface PluginSyncRunner<T, V> {
export interface PluginContext {
/** Plugin context properties */
[key: string]: any;
/** Logger (optional, fallback to console logger if undefined) */
/**
* Logger
* (optional, fallback to console logger if undefined)
*/
logger?: Logger;
/** Name of the API */
apiName?: string;
}

/**
Expand Down
3 changes: 0 additions & 3 deletions packages/@ama-sdk/core/src/plugins/core/reply-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ export interface ReplyPluginContext<T> extends PluginContext {
/** Type of the API */
apiType: ApiTypes | string;

/** Name of the API */
apiName?: string;

/** Exception thrown during call/parse of the response */
exception?: Error;

Expand Down
66 changes: 66 additions & 0 deletions packages/@ama-sdk/core/src/utils/generic-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type Api, type ApiClient, ApiTypes, type RequestOptionsParameters, type ReviverType } from '../fwk';

/**
* Generic request to the API
*/
export interface GenericRequestOptions<T> extends Omit<RequestOptionsParameters, 'api' | 'headers'> {
/** API used to identify the call */
api?: RequestOptionsParameters['api'];
/** Custom headers to provide to the request */
headers?: RequestOptionsParameters['headers'];
/** Custom operation ID to identify the request */
operationId?: string;
/** Custom reviver to revive the response of the call */
revivers?: ReviverType<T> | undefined | { [key: number]: ReviverType<T> | undefined };
}

/**
* Generic request to the API
*/
export class GenericApi implements Api {
/** API name */
public static readonly apiName = 'GenericApi';

/** @inheritDoc */
public readonly apiName = GenericApi.apiName;


/** @inheritDoc */
public client: ApiClient;

/**
* Initialize your interface
* @param apiClient
* @params apiClient Client used to process call to the API
*/
constructor(apiClient: ApiClient) {
this.client = apiClient;
}

/**
* Process to request to the API in the context of the SDK
* @param requestOptions Option to provide to process to the call
*/
public async request<T>(requestOptions: GenericRequestOptions<T>): Promise<T> {
const metadataHeaderAccept = requestOptions.metadata?.headerAccept || 'application/json';
const headers: { [key: string]: string | undefined } = {
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': requestOptions.metadata?.headerContentType || 'application/json',
// eslint-disable-next-line @typescript-eslint/naming-convention
...(metadataHeaderAccept ? { 'Accept': metadataHeaderAccept } : {})
};

const requestParameters: RequestOptionsParameters = {
api: this,
headers,
...requestOptions
};
const options = this.client.getRequestOptions ?
await this.client.getRequestOptions(requestParameters) :
await this.client.prepareOptions(requestParameters.basePath, requestParameters.method, requestParameters.queryParams || {}, requestParameters.headers);
const url = this.client.prepareUrl(options.basePath, options.queryParams);

const ret = this.client.processCall<T>(url, options, ApiTypes.DEFAULT, requestOptions.api!.apiName, requestOptions.revivers, requestOptions.operationId);
return ret;
}
}
1 change: 1 addition & 0 deletions packages/@ama-sdk/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './encoder';
export * from './ie11';
export * from './json-token';
export * from './mime-types';
export * from './generic-api';
2 changes: 1 addition & 1 deletion packages/@ama-sdk/schematics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"minimatch": "~9.0.3",
"rxjs": "^7.8.1",
"semver": "^7.5.2",
"sway": "^2.0.6",
"tslib": "^2.6.2"
},
"devDependencies": {
Expand Down Expand Up @@ -106,6 +105,7 @@
"npm-run-all2": "^6.0.0",
"nx": "~18.3.0",
"onchange": "^7.0.2",
"openapi-types": "^12.0.0",
"pid-from-port": "^1.1.3",
"semver": "^7.5.2",
"ts-jest": "~29.1.2",
Expand Down
Loading

0 comments on commit 1d23e04

Please sign in to comment.