Skip to content

Commit

Permalink
feat(sdk): provide a way to request a custom URL
Browse files Browse the repository at this point in the history
feat(sdk): add apiName in the request plugin context
  • Loading branch information
kpanot committed Apr 1, 2024
1 parent 348b829 commit 2202aa8
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 57 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
40 changes: 24 additions & 16 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 Down Expand Up @@ -58,24 +58,17 @@ 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 async 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);
Expand All @@ -90,6 +83,21 @@ export class ApiBeaconClient implements ApiClient {
return Promise.resolve(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 }): 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
*/
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
63 changes: 63 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,63 @@
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 };
}

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';
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class {{classname}} implements Api {
data['{{baseName}}'] = data['{{baseName}}'] !== undefined ? data['{{baseName}}'] : {{#isString}}'{{/isString}}{{defaultValue}}{{#isString}}'{{/isString}};
{{/defaultValue}}
{{/allParams}}
const getParams = this.client.extractQueryParams<{{#uppercaseFirst}}{{nickname}}{{/uppercaseFirst}}RequestData>(data, [{{#trimComma}}{{#queryParams}}'{{baseName}}', {{/queryParams}}{{/trimComma}}]{{^queryParams}} as never[]{{/queryParams}});
const queryParams = this.client.extractQueryParams<{{#uppercaseFirst}}{{nickname}}{{/uppercaseFirst}}RequestData>(data, [{{#trimComma}}{{#queryParams}}'{{baseName}}', {{/queryParams}}{{/trimComma}}]{{^queryParams}} as never[]{{/queryParams}});
const metadataHeaderAccept = metadata?.headerAccept || '{{#headerJsonMimeType}}{{#produces}}{{mediaType}}{{^-last}}, {{/-last}}{{/produces}}{{/headerJsonMimeType}}';
const headers: { [key: string]: string | undefined } = { {{#trimComma}}
'Content-Type': metadata?.headerContentType || '{{#headerJsonMimeType}}{{#consumes}}{{mediaType}}{{^-last}}, {{/-last}}{{/consumes}}{{/headerJsonMimeType}}',
Expand All @@ -100,11 +100,22 @@ export class {{classname}} implements Api {
body = data['{{baseName}}'] as any;
}
{{/bodyParam}}
const basePathUrl = `${this.client.options.basePath}{{#urlParamReplacer}}{{path}}{{/urlParamReplacer}}`;
const basePath = `${this.client.options.basePath}{{#urlParamReplacer}}{{path}}{{/urlParamReplacer}}`;
const tokenizedUrl = `${this.client.options.basePath}{{#tokenizedUrlParamReplacer}}{{path}}{{/tokenizedUrlParamReplacer}}`;
const tokenizedOptions = this.client.tokenizeRequestOptions(tokenizedUrl, getParams, this.piiParamTokens, data);
const tokenizedOptions = this.client.tokenizeRequestOptions(tokenizedUrl, queryParams, this.piiParamTokens, data);

const options = await this.client.prepareOptions(basePathUrl, '{{httpMethod}}', getParams, headers, body || undefined, tokenizedOptions, metadata);
const requestOptions = {
headers,
method: '{{httpMethod}}',
basePath,
queryParams,
body: body || undefined,
metadata,
tokenizedOptions,
api: this
};

const options = this.client.getRequestOptions : await this.client.getRequestOptions(requestOptions) ? await this.client.prepareOptions(basePath, '{{httpMethod}}', queryParams, headers, body || undefined, tokenizedOptions, metadata);
const url = this.client.prepareUrl(options.basePath, options.queryParams);

const ret = this.client.processCall<{{#vendorExtensions}}{{#responses2xxReturnTypes}}{{.}}{{^-last}} | {{/-last}}{{/responses2xxReturnTypes}}{{^responses2xxReturnTypes}}never{{/responses2xxReturnTypes}}{{/vendorExtensions}}>(url, options, {{#tags.0.extensions.x-api-type}}ApiTypes.{{tags.0.extensions.x-api-type}}{{/tags.0.extensions.x-api-type}}{{^tags.0.extensions.x-api-type}}ApiTypes.DEFAULT{{/tags.0.extensions.x-api-type}}, {{classname}}.apiName,{{#keepRevivers}}{{#vendorExtensions}}{{#responses2xx}}{{#-first}} { {{/-first}}{{code}}: {{^primitiveType}}revive{{baseType}}{{/primitiveType}}{{#primitiveType}}undefined{{/primitiveType}}{{^-last}}, {{/-last}}{{#-last}} } {{/-last}}{{/responses2xx}}{{^responses2xx}} undefined{{/responses2xx}}{{/vendorExtensions}}{{/keepRevivers}}{{^keepRevivers}} undefined{{/keepRevivers}}, '{{nickname}}');
Expand Down

0 comments on commit 2202aa8

Please sign in to comment.