@deejayy/api-caller
Version:
Simple Api Caller library for Angular
331 lines (318 loc) • 15.5 kB
JavaScript
import * as i1 from '@angular/common/http';
import { HttpErrorResponse, HttpHeaders, HttpClientModule } from '@angular/common/http';
import * as i0 from '@angular/core';
import { Optional, Injectable, NgModule } from '@angular/core';
import * as i2 from '@ngrx/store';
import { createAction, props, createFeatureSelector, createSelector, select, on, createReducer, StoreModule } from '@ngrx/store';
import { of } from 'rxjs';
import { take, mergeMap, takeUntil, filter, map, catchError } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import * as i1$1 from '@ngrx/effects';
import { ofType, createEffect, EffectsModule } from '@ngrx/effects';
import { enableMapSet, produce } from 'immer';
const apiStateId = '@deejayy/api-caller';
class ApiActions {
static { this.ApiGet = createAction('[API] Get', props()); }
static { this.ApiGetCancel = createAction('[API] Get Cancel', props()); }
static { this.ApiGetSuccess = createAction('[API] Get Success', props()); }
static { this.ApiGetFail = createAction('[API] Get Fail', props()); }
static { this.ApiGetFromCache = createAction('[API] Get From Cache', props()); }
static { this.ApiClearState = createAction('[API] Clear State', props()); }
static { this.ApiClearAllState = createAction('[API] Clear Full State'); }
}
/* eslint-disable @typescript-eslint/no-explicit-any */
const initialApiCallerGlobalState = {};
const initialApiCallerState = {
loading: false,
success: false,
error: false,
data: null,
headers: {},
errorData: new HttpErrorResponse({}),
fired: undefined,
returned: undefined,
};
const getStateId = (payload) => `${payload.api ?? ''}${payload.idOverride ?? payload.path}`;
const getApiState = createFeatureSelector(apiStateId);
const getApiSubState = (stateId) => createSelector(getApiState, (state) => state?.[stateId] ? state[stateId] ?? initialApiCallerState : initialApiCallerState);
class ApiSelectors {
static { this.isLoading = (stateId) => createSelector(getApiSubState(stateId), (state) => state.loading); }
static { this.getResponse = (stateId) => createSelector(getApiSubState(stateId), (state) => state.data); }
static { this.getHeaders = (stateId) => createSelector(getApiSubState(stateId), (state) => state.headers); }
static { this.getErrorData = (stateId) => createSelector(getApiSubState(stateId), (state) => state.errorData); }
static { this.isFailed = (stateId) => createSelector(getApiSubState(stateId), (state) => state.error); }
static { this.isSucceeded = (stateId) => createSelector(getApiSubState(stateId), (state) => state.success); }
static { this.isFinished = (stateId) => createSelector(getApiSubState(stateId), (state) => state.success || state.error); }
static { this.isCached = (stateId, cacheTimeout) => createSelector(getApiSubState(stateId), (state) => {
if (state.returned && cacheTimeout) {
if (new Date().getTime() - state.returned.getTime() > cacheTimeout) {
return false;
}
}
return state.data !== undefined && state.data !== null;
}); }
}
class ApiConnector {
}
class ApiCallerService {
constructor(http, store, apiConnector) {
this.http = http;
this.store = store;
this.apiConnector = apiConnector;
this.tokenData$ = of(`[${apiStateId}] Can't send requests with authorization, token provider not found`);
this.defaultApiUrl = '/';
this.errorHandler = (payload) => {
console.warn(`[${apiStateId}] Unhandled API error occurred, code: ${payload.response.status}`);
};
if (!this.apiConnector) {
console.warn(`[${apiStateId}] apiConnector not provided, check README.md`);
}
else {
this.tokenData$ = this.getTokenData();
this.defaultApiUrl = this.getDefaultApiUrl();
this.errorHandler = this.getErrorHandler();
}
}
getDefaultApiUrl() {
return this.apiConnector?.defaultApiUrl ?? this.defaultApiUrl;
}
getTokenData() {
return this.apiConnector?.tokenData$ ?? this.tokenData$;
}
getErrorHandler() {
return this.apiConnector?.errorHandler ?? this.errorHandler;
}
handleError(payload) {
if (!payload.request?.localErrorHandling) {
return this.getErrorHandler()(payload);
}
return 'Handled locally';
}
getApiCallPayload(apiCallItem) {
return {
payload: {
...apiCallItem,
api: apiCallItem.api ?? this.getDefaultApiUrl(),
},
};
}
callApi(apiCallItem) {
// Workaround to avoid "TypeError: Cannot freeze" error, native primitives (like FileList) cannot be passed to the state manager
// See: https://stackoverflow.com/a/53092520
if (apiCallItem.binaryUpload) {
apiCallItem.payload =
apiCallItem.payload && apiCallItem.payload.length > 0 ? { ...apiCallItem.payload } : undefined;
}
this.store.dispatch(ApiActions.ApiGet(this.getApiCallPayload(apiCallItem)));
return this.createApiResults(apiCallItem);
}
resetApi(apiCallItem) {
this.store.dispatch(ApiActions.ApiClearState(this.getApiCallPayload(apiCallItem)));
}
cancelRequest(apiCallItem) {
this.store.dispatch(ApiActions.ApiGetCancel(this.getApiCallPayload(apiCallItem)));
}
resetAllApi() {
this.store.dispatch(ApiActions.ApiClearAllState());
}
createApiResults(apiCallItem) {
const stateId = getStateId(this.getApiCallPayload(apiCallItem).payload);
return {
loading$: this.store.pipe(select(ApiSelectors.isLoading(stateId))),
data$: this.store.pipe(select(ApiSelectors.getResponse(stateId))),
errorData$: this.store.pipe(select(ApiSelectors.getErrorData(stateId))),
error$: this.store.pipe(select(ApiSelectors.isFailed(stateId))),
success$: this.store.pipe(select(ApiSelectors.isSucceeded(stateId))),
finished$: this.store.pipe(select(ApiSelectors.isFinished(stateId))),
headers$: this.store.pipe(select(ApiSelectors.getHeaders(stateId))),
};
}
// eslint-disable-next-line complexity
makeHeaders(call, options) {
let headers = new HttpHeaders();
if (call.binaryUpload) {
if (call.payload) {
const formData = new FormData();
formData.append(call.binaryUpload, call.payload[0]);
options.body = formData;
}
else {
console.warn(`[${apiStateId}] No file selected for upload but binaryUpload field name is set`);
}
}
if (call.payloadType === 'urlEncoded') {
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
const encodedData = Object.keys(call.payload)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(call.payload[key])}`)
.join('&');
options.body = encodedData;
}
if (call.payloadType === 'formData') {
const formData = new FormData();
Object.keys(call.payload).forEach((key) => formData.append(key, call.payload[key]));
options.body = formData;
}
if (call.extraHeaders && Object.keys(call.extraHeaders).length > 0) {
Object.keys(call.extraHeaders).forEach((key) => {
if (call.extraHeaders?.[key] !== undefined) {
headers.append(key, call.extraHeaders[key]);
}
});
}
return headers;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, complexity
makeRequest(call) {
const method = call.method ?? (call.payload ? 'POST' : 'GET');
const { api } = call;
const url = `${api ?? ''}${call.path}`;
const options = { body: call.payload, observe: 'response' };
const headers = this.makeHeaders(call, options);
if (call.binaryResponse) {
options.responseType = 'blob';
}
if (call.sendCookies) {
options.withCredentials = true;
}
if (call.needsAuth) {
return this.getTokenData().pipe(take(1), mergeMap((token) => {
options.headers = headers.set('Authorization', `Bearer ${token}`);
return this.http.request(method, url, options);
}));
}
return this.http.request(method, url, options);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerService, deps: [{ token: i1.HttpClient }, { token: i2.Store }, { token: ApiConnector, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.Store }, { type: ApiConnector, decorators: [{
type: Optional
}] }] });
enableMapSet();
const produceOn = (actionType, callback) => on(actionType, (state, action) => produce(state, (draft) => callback(draft, action)));
const apiGet = (draft, action) => {
const stateId = getStateId(action.payload);
draft[stateId] = {
...(draft[stateId] ?? initialApiCallerState),
loading: true,
error: false,
success: false,
fired: new Date(),
};
};
const apiGetSuccess = (draft, action) => {
const stateId = getStateId(action.request);
draft[stateId] = {
...(draft[stateId] ?? initialApiCallerState),
loading: false,
error: false,
success: true,
returned: new Date(),
headers: action.headers,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: action.response?.body,
};
};
const apiGetFail = (draft, action) => {
const stateId = getStateId(action.request);
draft[stateId] = {
...(draft[stateId] ?? initialApiCallerState),
loading: false,
error: true,
success: false,
returned: new Date(),
headers: action.headers,
errorData: action.response,
};
};
const apiGetFromCache = (draft, action) => {
const stateId = getStateId(action.payload);
draft[stateId] = {
...(draft[stateId] ?? initialApiCallerState),
loading: false,
error: false,
success: true,
};
};
const apiClearState = (draft, action) => {
const stateId = getStateId(action.payload);
draft[stateId] = initialApiCallerState;
};
const apiClearAllState = () => ({});
const apiReducer = createReducer(initialApiCallerGlobalState, produceOn(ApiActions.ApiGet, apiGet), produceOn(ApiActions.ApiGetSuccess, apiGetSuccess), produceOn(ApiActions.ApiGetFail, apiGetFail), produceOn(ApiActions.ApiGetFromCache, apiGetFromCache), produceOn(ApiActions.ApiClearState, apiClearState), produceOn(ApiActions.ApiClearAllState, apiClearAllState));
class ApiEffects {
parseHeaders(response) {
return response?.headers
?.keys()
.map((key) => {
return { [key]: response.headers.get(key) ?? '' };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
}
handleSuccess(request) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response) => {
return ApiActions.ApiGetSuccess({ request, response, headers: this.parseHeaders(response) });
};
}
handleError(request) {
return (response) => of(ApiActions.ApiGetFail({ request, response, headers: this.parseHeaders(response) }));
}
mergeWithCache(request) {
return (isCached) => {
return request.useCache && isCached
? of(ApiActions.ApiGetFromCache({ payload: request }))
: this.apiService
.makeRequest(request)
.pipe(takeUntil(this.actions$.pipe(ofType(ApiActions.ApiGetCancel), filter((cancelledRequest) => getStateId(cancelledRequest.payload) === getStateId(request)))))
.pipe(map(this.handleSuccess(request)), catchError(this.handleError(request)));
};
}
constructor(actions$, apiService, store) {
this.actions$ = actions$;
this.apiService = apiService;
this.store = store;
this.getApiEffect = ({ payload }) => {
const stateId = getStateId(payload);
return this.store
.pipe(select(ApiSelectors.isCached(stateId, payload.cacheTimeout)))
.pipe(take(1), mergeMap(this.mergeWithCache(payload)));
};
this.getApi$ = createEffect(() => this.actions$.pipe(ofType(ApiActions.ApiGet), mergeMap(this.getApiEffect)));
this.getApiFail$ = createEffect(() => this.actions$.pipe(ofType(ApiActions.ApiGetFail), map((action) => this.apiService.handleError(action))), { dispatch: false });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiEffects, deps: [{ token: i1$1.Actions }, { token: ApiCallerService }, { token: i2.Store }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiEffects }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiEffects, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1$1.Actions }, { type: ApiCallerService }, { type: i2.Store }] });
class ApiCallerModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerModule, imports: [CommonModule,
HttpClientModule, i2.StoreFeatureModule, i1$1.EffectsFeatureModule] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerModule, providers: [ApiCallerService], imports: [CommonModule,
HttpClientModule,
StoreModule.forFeature(apiStateId, apiReducer),
EffectsModule.forFeature([ApiEffects])] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ApiCallerModule, decorators: [{
type: NgModule,
args: [{
declarations: [],
imports: [
CommonModule,
HttpClientModule,
StoreModule.forFeature(apiStateId, apiReducer),
EffectsModule.forFeature([ApiEffects]),
],
providers: [ApiCallerService],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ApiCallerModule, ApiCallerService, ApiConnector, initialApiCallerGlobalState, initialApiCallerState };
//# sourceMappingURL=deejayy-api-caller.mjs.map