UNPKG

@deejayy/api-caller

Version:

Simple Api Caller library for Angular

331 lines (318 loc) 15.5 kB
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