@sword-health/ui-http-mapper
Version:
UI clients tool for consumption of easy to read/config endpoint maps
136 lines (108 loc) • 4.77 kB
text/typescript
const defaultConfigs = {
logPrefix: '[sh-http]',
supportedMethods: ['get', 'post', 'put', 'delete', 'head', 'patch', 'options'],
};
// Workaround for array based dynamic type creation
const auxDynamicMethodTypesCreation = Object.assign({}, ...defaultConfigs.supportedMethods.map(t => ({[t]: ''})) );
type requestMethod = keyof typeof auxDynamicMethodTypesCreation;
interface RequestData {
method: requestMethod;
url: string;
requestArgs: Array<object>;
meta: object;
}
interface EndpointsConfigs {
data?: object;
modules: object;
}
interface EndpointRequirements {
$$method: requestMethod;
$$makeUrl: Function;
$$config: object;
[key: string]: any,
}
type FoundEnpointConfig = EndpointRequirements | void;
export default class EndpointDecoderTS {
endpointsSpec: EndpointsConfigs;
constructor(endpointsMap: EndpointsConfigs = { modules: {} }, configs: object = {}) {
this.endpointsSpec = endpointsMap;
}
private _makeNamespacePath(namespace: string): Array<string> {
const safeNameSpace = namespace.replace(/(\/{2})|(\/$)/, '');
return safeNameSpace.split('/');
}
private _findEndpointConfigs(namespace: string, endpointsConfigsMap: object = {}): FoundEnpointConfig {
const pathSteps = this._makeNamespacePath(namespace);
if (!pathSteps.length) {
return;
}
const pathToConsider = pathSteps.join('/');
const goNextStep = (steps: Array<string>, currentConfigs: object, currIndex: number = 0): FoundEnpointConfig => {
const stepConfigs = currentConfigs[steps[currIndex]];
if (!stepConfigs) {
console.error(`${defaultConfigs.logPrefix} Unable to find configs for '${pathToConsider}'`);
return;
}
// Check if should end querying
const reachFinalStep = steps.length - 1 === currIndex;
// Keep finding until reaching end of given path
if (!reachFinalStep) {
// eslint-disable-next-line consistent-return
return goNextStep(steps, stepConfigs, currIndex + 1);
}
// Valid if found path is a complete one
const validFinalPath = ('$$makeUrl' in stepConfigs) && ('$$method' in stepConfigs);
if (!validFinalPath) {
console.error(`${defaultConfigs.logPrefix} Given path '${pathToConsider}' incomplete.`);
return;
}
// eslint-disable-next-line consistent-return
return stepConfigs;
};
// eslint-disable-next-line consistent-return
return goNextStep(pathSteps, endpointsConfigsMap);
}
set dataUpdate(freshDataObject: object) {
this.endpointsSpec.data = freshDataObject;
}
$http(namespace: string, urlData: any, { body: requestPayload, config: requestConfigs }: { body?: object, config?: object } = {config: {}}): RequestData {
// Validate namespace format
const validNameSpace = typeof namespace === 'string';
if (!validNameSpace) {
console.error(`${defaultConfigs.logPrefix} Invalid namespace provided. Aborting`);
return;
}
// Find requested endpoint configuration
const endpointConfigs = this._findEndpointConfigs(namespace, this.endpointsSpec.modules);
if (!endpointConfigs) {
return;
}
const { $$method: method, $$makeUrl: makeUrl, $$config: defaultConfig = {}, $$meta: meta = {} } = endpointConfigs;
// Check if what config from endpoint spec are executable and run them now
const defaultConfigExec = {};
Object.keys(defaultConfig).forEach(configKey => {
const configVal = defaultConfig[configKey];
const executableConfig = (typeof configVal === 'function');
if (executableConfig) {
defaultConfigExec[configKey] = configVal(this.endpointsSpec.data);
return;
}
defaultConfigExec[configKey] = configVal;
});
// Merge default configs with adhoc one, giving priority to adhoc ones
const mergedConfig = Object.assign({}, defaultConfigExec, requestConfigs);
// Make endpoint url (will use default base url)
const url = makeUrl(endpointConfigs, urlData);
// Appending request payload to requests that support it
const requestArgs = [mergedConfig];
if (method === 'put' || method === 'post' || method === 'patch' || method === 'delete') {
requestArgs.unshift(requestPayload);
}
return {
method,
url,
requestArgs,
meta,
}
}
}