Skip to content
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

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

Merged
merged 2 commits into from
Apr 23, 2024
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
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 {
kpanot marked this conversation as resolved.
Show resolved Hide resolved
/** 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
Loading