From f84fc340e46ef9423dfffee838cabee3085eaf2c Mon Sep 17 00:00:00 2001 From: prhodesy Date: Thu, 2 Sep 2021 13:30:18 +0700 Subject: [PATCH 01/36] created http error interceptor --- src/app/app.module.ts | 11 ++++++-- .../http-error.interceptor.spec.ts | 16 ++++++++++++ .../interceptors/http-error.interceptor.ts | 25 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/app/interceptors/http-error.interceptor.spec.ts create mode 100644 src/app/interceptors/http-error.interceptor.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3b8ea2f..a72f516 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; @@ -19,6 +19,7 @@ import { CreateTodoComponent } from './components/todos/create-todo/create-todo. import { UpdateTodoComponent } from './components/todos/update-todo/update-todo.component'; import { TodoFormComponent } from './components/todos/todo-form/todo-form.component'; import { ModalComponent } from './components/modal/modal.component'; +import { HttpErrorInterceptor } from './interceptors/http-error.interceptor'; @NgModule({ declarations: [ @@ -44,7 +45,13 @@ import { ModalComponent } from './components/modal/modal.component'; HttpClientModule, FormsModule, ], - providers: [], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: HttpErrorInterceptor, + multi: true + } + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/interceptors/http-error.interceptor.spec.ts b/src/app/interceptors/http-error.interceptor.spec.ts new file mode 100644 index 0000000..ad9d42d --- /dev/null +++ b/src/app/interceptors/http-error.interceptor.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HttpErrorInterceptor } from './http-error.interceptor'; + +describe('HttpErrorInterceptor', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + HttpErrorInterceptor + ] + })); + + it('should be created', () => { + const interceptor: HttpErrorInterceptor = TestBed.inject(HttpErrorInterceptor); + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts new file mode 100644 index 0000000..c19501e --- /dev/null +++ b/src/app/interceptors/http-error.interceptor.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, + HttpErrorResponse +} from '@angular/common/http'; +import { Observable, pipe, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +@Injectable() +export class HttpErrorInterceptor implements HttpInterceptor { + + constructor() {} + + intercept(request: HttpRequest, next: HttpHandler): Observable> { + return next.handle(request).pipe( + catchError((error: HttpErrorResponse) => { + console.log('intercepted error response'); + return throwError('error') + }) + ); + } +} From bcb1324b2263a18bf6cec6956dbbd85d9f60c8cc Mon Sep 17 00:00:00 2001 From: prhodesy Date: Thu, 2 Sep 2021 13:50:32 +0700 Subject: [PATCH 02/36] moved error handling from service to interceptor --- src/app/interceptors/http-error.interceptor.ts | 9 +++++++-- src/app/services/api.service.ts | 12 +----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index c19501e..0a8c77c 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -17,8 +17,13 @@ export class HttpErrorInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { - console.log('intercepted error response'); - return throwError('error') + let errorMessage = ''; + if(error.error instanceof ErrorEvent) { // client-side error + errorMessage = error.error.message; + } else { // server-side error + errorMessage = `Error status: ${error.status}, error message: ${error.message}`; + } + return throwError(errorMessage); }) ); } diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 09faf97..da2e54f 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -88,7 +88,7 @@ export class ApiService { } return obs.pipe( - catchError(this.errorHandler) + //catchError(this.errorHandler) ); } @@ -101,14 +101,4 @@ export class ApiService { } return JSON.stringify(obj); } - - private errorHandler(error: HttpErrorResponse): Observable { - let errorMessage = ''; - if(error.error instanceof ErrorEvent) { // client-side error - errorMessage = error.error.message; - } else { // server-side error - errorMessage = `Error status: ${error.status}, error message: ${error.message}`; - } - return throwError(errorMessage); - } } From 653a5589867e4cc6d9f551e3b8031c453b8c307f Mon Sep 17 00:00:00 2001 From: prhodesy Date: Thu, 2 Sep 2021 14:43:47 +0700 Subject: [PATCH 03/36] customized server error message, commented out console logs --- src/app/components/modal/modal.component.ts | 4 ++-- .../interceptors/http-error.interceptor.ts | 23 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/app/components/modal/modal.component.ts b/src/app/components/modal/modal.component.ts index 5764923..94f2ba9 100644 --- a/src/app/components/modal/modal.component.ts +++ b/src/app/components/modal/modal.component.ts @@ -19,9 +19,9 @@ export class ModalComponent { modalRef.componentInstance.body = body; modalRef.result.then((res) => { - console.log(`Closed with: ${res}`); + //console.log(`Closed with: ${res}`); }, (res) => { - console.log(`Dismissed ${ModalComponent.getDismissReason(res)}`); + //console.log(`Dismissed ${ModalComponent.getDismissReason(res)}`); }); } diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index 0a8c77c..e47e1bc 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -17,14 +17,23 @@ export class HttpErrorInterceptor implements HttpInterceptor { intercept(request: HttpRequest, next: HttpHandler): Observable> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { - let errorMessage = ''; - if(error.error instanceof ErrorEvent) { // client-side error - errorMessage = error.error.message; - } else { // server-side error - errorMessage = `Error status: ${error.status}, error message: ${error.message}`; - } - return throwError(errorMessage); + let errorMessage: string; + if(error.error instanceof ErrorEvent) { // client-side error + errorMessage = error.error.message; + } else { // server-side error + errorMessage = this.getServerErrorMessage(error); + } + return throwError(errorMessage); }) ); } + + private getServerErrorMessage(error: HttpErrorResponse): string { + switch (error.status) { + case 404: + return 'Not found'; + default: + return `Error status: ${error.status}, error message: ${error.message}`; + } + } } From 1251011a889349a66484188ac4b4831fc5d10132 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Thu, 2 Sep 2021 15:22:38 +0700 Subject: [PATCH 04/36] customized client error message --- src/app/interceptors/http-error.interceptor.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index e47e1bc..5d6533d 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -18,8 +18,8 @@ export class HttpErrorInterceptor implements HttpInterceptor { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { let errorMessage: string; - if(error.error instanceof ErrorEvent) { // client-side error - errorMessage = error.error.message; + if(error.status === 0) { // client-side or network error + errorMessage = this.getClientNetworkErrorMessage(error); } else { // server-side error errorMessage = this.getServerErrorMessage(error); } @@ -28,10 +28,17 @@ export class HttpErrorInterceptor implements HttpInterceptor { ); } + private getClientNetworkErrorMessage(error: HttpErrorResponse): string { + if (!navigator.onLine) { + return 'No internet connection'; + } + return error.error.message; + } + private getServerErrorMessage(error: HttpErrorResponse): string { switch (error.status) { case 404: - return 'Not found'; + return `Not found: ${error.message}`; default: return `Error status: ${error.status}, error message: ${error.message}`; } From 7cd78ee7c52ee3d3f915fae1fecc56271da381bb Mon Sep 17 00:00:00 2001 From: prhodesy Date: Thu, 2 Sep 2021 16:01:24 +0700 Subject: [PATCH 05/36] changed modal to accept string array --- src/app/components/modal/modal.component.html | 4 +-- src/app/components/modal/modal.component.ts | 13 ++++--- src/app/components/todos/todos.component.ts | 36 +++++++++---------- src/app/components/users/users.component.ts | 2 +- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/app/components/modal/modal.component.html b/src/app/components/modal/modal.component.html index 2cb07f8..9e83c52 100644 --- a/src/app/components/modal/modal.component.html +++ b/src/app/components/modal/modal.component.html @@ -4,8 +4,8 @@ - + + + +

Request details

+
    +
  • {{detail}}
  • +
diff --git a/src/app/components/http-status-codes/http-status-codes.component.ts b/src/app/components/http-status-codes/http-status-codes.component.ts index 2ab27b3..eacaab3 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.ts +++ b/src/app/components/http-status-codes/http-status-codes.component.ts @@ -1,26 +1,52 @@ -import { Component, OnInit } from '@angular/core'; -import { ReasonPhrases, StatusCodes, getReasonPhrase, getStatusCode } from 'http-status-codes'; +import { Component } from '@angular/core'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { ApiService } from 'src/app/services/api.service'; @Component({ selector: 'app-http-status-codes', templateUrl: './http-status-codes.component.html', styleUrls: ['./http-status-codes.component.scss'] }) -export class HttpStatusCodesComponent implements OnInit { +export class HttpStatusCodesComponent { public httpStatusCodes: number[]; public selectedHttpStatusCode: number; + public requestDetails: string[]; + private delayMilliseconds?: number; - constructor() { + constructor(private apiService: ApiService) { this.httpStatusCodes = Object.values(StatusCodes) .filter(value => typeof value === 'number') .map(value => value as number) .sort((n1,n2) => n1 - n2); this.selectedHttpStatusCode = this.httpStatusCodes[0]; - console.log(this.httpStatusCodes) + this.requestDetails = []; + this.delayMilliseconds = 2500; } - ngOnInit(): void { + public performRequest(statusCode: number) { + this.requestDetails = []; + this.requestDetails.push(this.getCurrentDateTime()); + + let url: string = this.getUrl(statusCode); + this.requestDetails.push(`Performing request for url: ${url}`); + + this.apiService + .getOne(url) + .subscribe( + data => { + this.requestDetails.push(this.getCurrentDateTime()); + this.requestDetails.push('Received data.'); + }, + error => { + this.requestDetails.push(this.getCurrentDateTime()); + this.requestDetails.push('An error was thrown.'); + this.requestDetails.push(error); + }) + .add(() => { + this.requestDetails.push(this.getCurrentDateTime()); + this.requestDetails.push('Request finished.'); + }); } public getStatusCodeDescription(statusCode: number): string { @@ -30,4 +56,18 @@ export class HttpStatusCodesComponent implements OnInit { public changeSelectedHttpStatusCode(statusCode: number) { this.selectedHttpStatusCode = statusCode; } + + /* private methods */ + + private getUrl(statusCode: number): string { + let url: string = `https://httpstat.us/${statusCode}/cors`; + if (this.delayMilliseconds) { + url += `?sleep=${this.delayMilliseconds!}`; + } + return url; + } + + private getCurrentDateTime(): string { + return new Date().toLocaleString(); + } } From 1f99aea0c1a8913cb139cd0bcee77637884f831e Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 4 Sep 2021 01:45:52 +0700 Subject: [PATCH 13/36] formatted request details --- .../http-status-codes.component.html | 2 +- .../http-status-codes.component.ts | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/components/http-status-codes/http-status-codes.component.html b/src/app/components/http-status-codes/http-status-codes.component.html index f3255f7..647dadf 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.html +++ b/src/app/components/http-status-codes/http-status-codes.component.html @@ -18,5 +18,5 @@

HTTP status codes

Request details

    -
  • {{detail}}
  • +
  • {{line}}

diff --git a/src/app/components/http-status-codes/http-status-codes.component.ts b/src/app/components/http-status-codes/http-status-codes.component.ts index eacaab3..5f3fd3d 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.ts +++ b/src/app/components/http-status-codes/http-status-codes.component.ts @@ -11,7 +11,7 @@ export class HttpStatusCodesComponent { public httpStatusCodes: number[]; public selectedHttpStatusCode: number; - public requestDetails: string[]; + public requestDetails: string[][]; private delayMilliseconds?: number; constructor(private apiService: ApiService) { @@ -21,31 +21,26 @@ export class HttpStatusCodesComponent { .sort((n1,n2) => n1 - n2); this.selectedHttpStatusCode = this.httpStatusCodes[0]; this.requestDetails = []; - this.delayMilliseconds = 2500; + this.delayMilliseconds = 500; } public performRequest(statusCode: number) { this.requestDetails = []; - this.requestDetails.push(this.getCurrentDateTime()); let url: string = this.getUrl(statusCode); - this.requestDetails.push(`Performing request for url: ${url}`); + this.addRequestDetail(['Performing request.', url]); this.apiService .getOne(url) .subscribe( data => { - this.requestDetails.push(this.getCurrentDateTime()); - this.requestDetails.push('Received data.'); + this.addRequestDetail(['Received data.']); }, error => { - this.requestDetails.push(this.getCurrentDateTime()); - this.requestDetails.push('An error was thrown.'); - this.requestDetails.push(error); + this.addRequestDetail(['An error was thrown.', error]); }) .add(() => { - this.requestDetails.push(this.getCurrentDateTime()); - this.requestDetails.push('Request finished.'); + this.addRequestDetail(['Request finished.']); }); } @@ -67,6 +62,10 @@ export class HttpStatusCodesComponent { return url; } + private addRequestDetail(detail: string[]) { + this.requestDetails.push([this.getCurrentDateTime(), ...detail]); + } + private getCurrentDateTime(): string { return new Date().toLocaleString(); } From d7db071d573f026d3f6d9ddc7a22bec7a2cb94db Mon Sep 17 00:00:00 2001 From: prhodesy Date: Tue, 7 Sep 2021 20:18:45 +0700 Subject: [PATCH 14/36] formatted page --- .../http-status-codes.component.html | 51 ++++++++++++------- .../http-status-codes.component.ts | 18 +++---- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/app/components/http-status-codes/http-status-codes.component.html b/src/app/components/http-status-codes/http-status-codes.component.html index 647dadf..f6eb92d 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.html +++ b/src/app/components/http-status-codes/http-status-codes.component.html @@ -1,22 +1,37 @@

HTTP status codes

-
- -
- +

Select a HTTP status code from the dropdown below. Then click the 'Request' button to perform a request and receive a response with the selected code.

+ +
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+

Request log

+

Perform a request to see the log

+ +
    +
  • {{line}}

  • +
+
+
- - - -

Request details

-
    -
  • {{line}}

  • -
diff --git a/src/app/components/http-status-codes/http-status-codes.component.ts b/src/app/components/http-status-codes/http-status-codes.component.ts index 5f3fd3d..02e8c32 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.ts +++ b/src/app/components/http-status-codes/http-status-codes.component.ts @@ -11,7 +11,7 @@ export class HttpStatusCodesComponent { public httpStatusCodes: number[]; public selectedHttpStatusCode: number; - public requestDetails: string[][]; + public requestLog: string[][]; private delayMilliseconds?: number; constructor(private apiService: ApiService) { @@ -20,27 +20,27 @@ export class HttpStatusCodesComponent { .map(value => value as number) .sort((n1,n2) => n1 - n2); this.selectedHttpStatusCode = this.httpStatusCodes[0]; - this.requestDetails = []; + this.requestLog = []; this.delayMilliseconds = 500; } public performRequest(statusCode: number) { - this.requestDetails = []; + this.requestLog = []; let url: string = this.getUrl(statusCode); - this.addRequestDetail(['Performing request.', url]); + this.addRequestLog(['Performing request.', url]); this.apiService .getOne(url) .subscribe( data => { - this.addRequestDetail(['Received data.']); + this.addRequestLog(['Received data.']); }, error => { - this.addRequestDetail(['An error was thrown.', error]); + this.addRequestLog(['An error was thrown.', error]); }) .add(() => { - this.addRequestDetail(['Request finished.']); + this.addRequestLog(['Request finished.']); }); } @@ -62,8 +62,8 @@ export class HttpStatusCodesComponent { return url; } - private addRequestDetail(detail: string[]) { - this.requestDetails.push([this.getCurrentDateTime(), ...detail]); + private addRequestLog(detail: string[]) { + this.requestLog.push([this.getCurrentDateTime(), ...detail]); } private getCurrentDateTime(): string { From 0562d132a19b9ed703dde012f7a930d067cc41c9 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Tue, 7 Sep 2021 21:27:45 +0700 Subject: [PATCH 15/36] added redirect to login functionality --- src/app/app-routing.module.ts | 2 ++ src/app/app.module.ts | 4 ++- .../http-status-codes.component.html | 6 ++++ src/app/components/login/login.component.ts | 12 +++++++ .../interceptors/http-error.interceptor.ts | 35 +++++++++++++------ 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 src/app/components/login/login.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f7e3d22..566f0c0 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from 'src/app/components/home/home.component'; import { HttpStatusCodesComponent } from 'src/app/components/http-status-codes/http-status-codes.component'; +import { LoginComponent } from 'src/app/components/login/login.component'; import { NotFoundComponent } from 'src/app/components/not-found/not-found.component'; import { TodosComponent } from 'src/app/components/todos/todos.component'; import { UsersComponent } from 'src/app/components/users/users.component'; @@ -11,6 +12,7 @@ const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, // Redirect empty url to 'home' { path: 'home', component: HomeComponent }, { path: 'http-status-codes', component: HttpStatusCodesComponent }, + { path: 'login', component: LoginComponent }, { path: 'todos', component: TodosComponent }, { path: 'users', component: UsersComponent }, { path: '**', component: NotFoundComponent } // Wildcard route for a 404 not found, must be last element in the list diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 929f221..9e35326 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -21,6 +21,7 @@ import { TodoFormComponent } from './components/todos/todo-form/todo-form.compon import { ModalComponent } from './components/modal/modal.component'; import { HttpErrorInterceptor } from './interceptors/http-error.interceptor'; import { HttpStatusCodesComponent } from './components/http-status-codes/http-status-codes.component'; +import { LoginComponent } from './components/login/login.component'; @NgModule({ declarations: [ @@ -37,7 +38,8 @@ import { HttpStatusCodesComponent } from './components/http-status-codes/http-st UpdateTodoComponent, TodoFormComponent, ModalComponent, - HttpStatusCodesComponent + HttpStatusCodesComponent, + LoginComponent ], imports: [ BrowserModule, diff --git a/src/app/components/http-status-codes/http-status-codes.component.html b/src/app/components/http-status-codes/http-status-codes.component.html index f6eb92d..97e41bd 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.html +++ b/src/app/components/http-status-codes/http-status-codes.component.html @@ -2,6 +2,12 @@

HTTP status codes

Select a HTTP status code from the dropdown below. Then click the 'Request' button to perform a request and receive a response with the selected code.

+
    +
  • 401, 403: redirect to login page
  • +
  • 404: custom error message
  • +
  • 408, 500, 503, 504: retry request
  • +
+
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts new file mode 100644 index 0000000..dcb453e --- /dev/null +++ b/src/app/components/login/login.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-login', + template: ` +

Login

+

Dummy login page.

+ `, + styles: [ + ] +}) +export class LoginComponent {} diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index 9f11959..2b402a6 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -3,6 +3,7 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest import { Observable, of, pipe, throwError } from 'rxjs'; import { catchError, concatMap, delay, retryWhen } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; +import { Router } from '@angular/router'; @Injectable() export class HttpErrorInterceptor implements HttpInterceptor { @@ -12,7 +13,7 @@ export class HttpErrorInterceptor implements HttpInterceptor { private retryInitialIntervalMs: number; private retryExpDelayBase: number; - constructor() { + constructor(private router: Router) { this.retryAttempts = environment.HTTP_ERROR_RETRY_ATTEMPTS; this.retryStatusCodes = environment.HTTP_ERROR_RETRY_STATUS_CODES; this.retryInitialIntervalMs = environment.HTTP_ERROR_RETRY_INITIAL_INTERVAL_MILLISECONDS; @@ -32,8 +33,7 @@ export class HttpErrorInterceptor implements HttpInterceptor { return errors.pipe( // Handle the errors in order concatMap((error, index) => { - if (index < this.retryAttempts && this.retryStatusCodes.includes(error.status)) { - // Retry with delay + if (index < this.retryAttempts && this.retryStatusCodes.includes(error.status)) { // retry with delay let delayMs: number = this.calculateDelayMs(index); return of(error).pipe(delay(delayMs)); } @@ -49,13 +49,19 @@ export class HttpErrorInterceptor implements HttpInterceptor { /* handle error */ private handleError(error: HttpErrorResponse): Observable { - let errorMessage: string; + let errorMessage: string | undefined = this.getErrorMessage(error); + if (!errorMessage) { // error was handled + return of(error); + } + return throwError(errorMessage); + } + + private getErrorMessage(error: HttpErrorResponse): string | undefined { if(error.status === 0) { // client-side or network error - errorMessage = this.getClientNetworkErrorMessage(error); + return this.getClientNetworkErrorMessage(error); } else { // server-side error - errorMessage = this.getServerErrorMessage(error); + return this.getServerErrorMessage(error); } - return throwError(errorMessage); } private getClientNetworkErrorMessage(error: HttpErrorResponse): string { @@ -65,12 +71,21 @@ export class HttpErrorInterceptor implements HttpInterceptor { return error.error.message; } - private getServerErrorMessage(error: HttpErrorResponse): string { + private getServerErrorMessage(error: HttpErrorResponse): string | undefined { + let errorMessage: string | undefined; + switch (error.status) { + case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: + this.router.navigateByUrl('/login'); + break; case HttpStatusCode.NotFound: - return `Not found: ${error.message}`; + errorMessage = 'N\u{1F621}T F\u{1F62D}UND'; + break; default: - return `Error status: ${error.status}, error message: ${error.message}`; + errorMessage = `Error status: ${error.status}, error message: ${error.message}`; } + + return errorMessage; } } From ffb9ae074fba22369f590d6d722ef86d10b5f513 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Tue, 7 Sep 2021 21:36:18 +0700 Subject: [PATCH 16/36] added loading spinner --- .../http-status-codes/http-status-codes.component.html | 1 + .../http-status-codes/http-status-codes.component.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/app/components/http-status-codes/http-status-codes.component.html b/src/app/components/http-status-codes/http-status-codes.component.html index 97e41bd..968f522 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.html +++ b/src/app/components/http-status-codes/http-status-codes.component.html @@ -38,6 +38,7 @@

HTTP status codes

  • {{line}}

  • +
    diff --git a/src/app/components/http-status-codes/http-status-codes.component.ts b/src/app/components/http-status-codes/http-status-codes.component.ts index 02e8c32..43d475a 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.ts +++ b/src/app/components/http-status-codes/http-status-codes.component.ts @@ -13,6 +13,7 @@ export class HttpStatusCodesComponent { public selectedHttpStatusCode: number; public requestLog: string[][]; private delayMilliseconds?: number; + public isPerformingRequest: boolean; constructor(private apiService: ApiService) { this.httpStatusCodes = Object.values(StatusCodes) @@ -22,10 +23,12 @@ export class HttpStatusCodesComponent { this.selectedHttpStatusCode = this.httpStatusCodes[0]; this.requestLog = []; this.delayMilliseconds = 500; + this.isPerformingRequest = false; } public performRequest(statusCode: number) { this.requestLog = []; + this.isPerformingRequest = true; let url: string = this.getUrl(statusCode); this.addRequestLog(['Performing request.', url]); @@ -41,6 +44,7 @@ export class HttpStatusCodesComponent { }) .add(() => { this.addRequestLog(['Request finished.']); + this.isPerformingRequest = false; }); } From 5ddfa9d52d608a89f15e1e3020c9ccb597e997cf Mon Sep 17 00:00:00 2001 From: prhodesy Date: Tue, 7 Sep 2021 22:20:58 +0700 Subject: [PATCH 17/36] updated description --- src/app/components/home/home.component.html | 33 ++++++--------------- src/app/components/home/home.component.scss | 3 ++ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/app/components/home/home.component.html b/src/app/components/home/home.component.html index fb15078..5ed3c1c 100644 --- a/src/app/components/home/home.component.html +++ b/src/app/components/home/home.component.html @@ -1,30 +1,15 @@

    Angular generic API service

    -

    Demo of an Angular generic API service that uses the resources from JSON Placeholder

    -

    Todos

    +

    Angular demo project for a generic API service using JSON Placeholder and httpstat.us.

    -

    Examples of HTTP requests for the JSON Placeholder todos endpoint.

    +

    Todos

    -
      -
    • - List todos: -
        -
      • Get all: perform a HTTP GET request to read all the todos
      • -
      • Get filtered: perform a HTTP GET request with a query parameter to read all the todos filtered by user ID
      • -
      • Delete: perform a HTTP DELETE request to delete a todo by ID
      • -
      -
    • -
    • View todo: perform a HTTP GET request to read a single todo by ID
    • -
    • Create new: perform a HTTP POST request to create a new todo
    • -
    • - Update existing: -
        -
      • Update full: perform a HTTP PUT request to replace the original todo object
      • -
      • Update partial: perform a HTTP PATCH request to modify a todo object
      • -
      -
    • -
    +

    Examples of how to perform various HTTP request methods (DELETE, GET, PATCH, POST, PUT) for a specific endpoint by extending services/BaseApiEndpointService.

    -

    Users

    +

    Users

    -

    Demonstrating class-transformer mapping plain javascript objects returned from an API call to an instance of a class. The JSON Placeholder users endpoint is used to get the data.

    +

    Demonstrates class-transformer mapping of plain javascript objects returned from an API call to an instance of a class.

    + +

    HTTP status codes

    + +

    Exercise custom logic based on the HTTP response status code.

    diff --git a/src/app/components/home/home.component.scss b/src/app/components/home/home.component.scss index e69de29..362802f 100644 --- a/src/app/components/home/home.component.scss +++ b/src/app/components/home/home.component.scss @@ -0,0 +1,3 @@ +a { + color: inherit; +} From 5a99169c2e0380d6c509dfcd2d9e166499c067bd Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 13:13:28 +0700 Subject: [PATCH 18/36] updated description --- src/app/components/home/home.component.html | 6 +++--- src/app/components/home/home.component.scss | 2 +- .../http-status-codes.component.html | 18 +++++++++++++----- .../http-status-codes.component.scss | 5 +++++ .../navigation-bar.component.html | 2 +- src/app/components/todos/todos.component.html | 7 ++----- .../update-todo/update-todo.component.html | 1 + src/app/components/users/users.component.html | 2 ++ src/environments/environment.default.ts | 2 +- 9 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/app/components/home/home.component.html b/src/app/components/home/home.component.html index 5ed3c1c..2bfd69a 100644 --- a/src/app/components/home/home.component.html +++ b/src/app/components/home/home.component.html @@ -1,14 +1,14 @@

    Angular generic API service

    -

    Angular demo project for a generic API service using JSON Placeholder and httpstat.us.

    +

    Angular demo project for a generic API service using the following services: JSON Placeholder and httpstat.us.

    Todos

    -

    Examples of how to perform various HTTP request methods (DELETE, GET, PATCH, POST, PUT) for a specific endpoint by extending services/BaseApiEndpointService.

    +

    Examples of how to perform various HTTP request methods (DELETE, GET, PATCH, POST, PUT) for a specific endpoint by extending BaseApiEndpointService.

    Users

    -

    Demonstrates class-transformer mapping of plain javascript objects returned from an API call to an instance of a class.

    +

    Demonstrates class-transformer mapping of plain javascript objects returned from an API call to an instance of a class.

    HTTP status codes

    diff --git a/src/app/components/home/home.component.scss b/src/app/components/home/home.component.scss index 362802f..61b5df8 100644 --- a/src/app/components/home/home.component.scss +++ b/src/app/components/home/home.component.scss @@ -1,3 +1,3 @@ -a { +h2 a { color: inherit; } diff --git a/src/app/components/http-status-codes/http-status-codes.component.html b/src/app/components/http-status-codes/http-status-codes.component.html index 968f522..fbb4efb 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.html +++ b/src/app/components/http-status-codes/http-status-codes.component.html @@ -1,11 +1,19 @@

    HTTP status codes

    -

    Select a HTTP status code from the dropdown below. Then click the 'Request' button to perform a request and receive a response with the selected code.

    +

    Select a HTTP status code from the dropdown below. Then click the 'Request' button to perform a request and receive a response from httpstat.us with the selected code. Custom logic based on the response code is performed by HttpErrorInterceptor, e.g.

      -
    • 401, 403: redirect to login page
    • -
    • 404: custom error message
    • -
    • 408, 500, 503, 504: retry request
    • +
    • + + {{status}}, + : redirect to login page +
    • +
    • 404: custom error message
    • +
    • + + {{status}}, + : retry request (see the failed requests in the console: CTRL + SHIFT + J) +
    @@ -26,7 +34,7 @@

    HTTP status codes

    - +
    diff --git a/src/app/components/http-status-codes/http-status-codes.component.scss b/src/app/components/http-status-codes/http-status-codes.component.scss index e69de29..ef94c56 100644 --- a/src/app/components/http-status-codes/http-status-codes.component.scss +++ b/src/app/components/http-status-codes/http-status-codes.component.scss @@ -0,0 +1,5 @@ +span { + cursor:pointer; + color:blue; + text-decoration:underline; +} diff --git a/src/app/components/navigation-bar/navigation-bar.component.html b/src/app/components/navigation-bar/navigation-bar.component.html index bc86ed5..06aee2d 100644 --- a/src/app/components/navigation-bar/navigation-bar.component.html +++ b/src/app/components/navigation-bar/navigation-bar.component.html @@ -1,5 +1,5 @@
    +
    diff --git a/src/app/components/users/users.component.html b/src/app/components/users/users.component.html index cb3561d..8b14c02 100644 --- a/src/app/components/users/users.component.html +++ b/src/app/components/users/users.component.html @@ -4,6 +4,7 @@

    Users

    Users list

    +

    All users from the JSON Placeholder users endpoint.

    @@ -26,6 +27,7 @@

    Users list

    User {{ selectedUser ? selectedUser!.id : '' }} details

    +

    The response is transformed into an instance of the UserModel class.

    Confirm can call class method

    UserModel.getFirstName() and UserModel.getLastName():

    diff --git a/src/environments/environment.default.ts b/src/environments/environment.default.ts index 3fcae8d..c053863 100644 --- a/src/environments/environment.default.ts +++ b/src/environments/environment.default.ts @@ -22,7 +22,7 @@ export const defaultEnvironment = { HTTP_ERROR_RETRY_ATTEMPTS: 3, HTTP_ERROR_RETRY_STATUS_CODES: [ HttpStatusCode.RequestTimeout, // 408 - HttpStatusCode.InternalServerError, // 500 + HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout, // 504 ], From c410e9ca7b1d09bcab2b3da6817530a6b482dc99 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 13:54:35 +0700 Subject: [PATCH 19/36] refactored --- src/app/components/todos/todos.component.ts | 18 +++++++++--------- src/app/components/users/users.component.ts | 6 +++--- src/app/services/base-api-endpoint.service.ts | 19 +++++++++---------- ...rvice.ts => todos-api-endpoint.service.ts} | 2 +- ...rvice.ts => users-api-endpoint.service.ts} | 2 +- 5 files changed, 23 insertions(+), 24 deletions(-) rename src/app/services/{todo.service.ts => todos-api-endpoint.service.ts} (91%) rename src/app/services/{user.service.ts => users-api-endpoint.service.ts} (87%) diff --git a/src/app/components/todos/todos.component.ts b/src/app/components/todos/todos.component.ts index 6c93aa1..fb2e5fa 100644 --- a/src/app/components/todos/todos.component.ts +++ b/src/app/components/todos/todos.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { Subject } from 'rxjs'; -import { TodoService } from 'src/app/services/todo.service'; +import { TodosApiEndpointService } from 'src/app/services/todos-api-endpoint.service'; import { TodoModel } from 'src/app/models/todo.model'; import { AtLeastIdAndOneField } from 'src/app/models/base-api-endpoint.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -24,7 +24,7 @@ export class TodosComponent { public readonly createNavItemId: number = 3; public readonly updateNavItemId: number = 4; - constructor(private todoService: TodoService, private modal: NgbModal) { + constructor(private todosApiEndpointService: TodosApiEndpointService, private modal: NgbModal) { this.todos = []; this.isPerformingRequest = false; this.activeNavItem = this.listTodosNavItemId; @@ -39,7 +39,7 @@ export class TodosComponent { let modalTitle: string = 'Create todo'; try { - this.todoService + this.todosApiEndpointService .create(todo) .subscribe( data => { @@ -64,7 +64,7 @@ export class TodosComponent { this.isPerformingRequest = true; let modalTitle: string = 'Delete todo'; - this.todoService + this.todosApiEndpointService .delete(id) .subscribe( data => { @@ -86,7 +86,7 @@ export class TodosComponent { this.todos = []; let modalTitle: string = "Get all todos"; - this.todoService + this.todosApiEndpointService .getMany() .subscribe( data => { @@ -105,7 +105,7 @@ export class TodosComponent { this.todos = []; let modalTitle: string = "Get filtered todos"; - this.todoService + this.todosApiEndpointService .getManyFilterByUserId(userId) .subscribe( data => { @@ -131,7 +131,7 @@ export class TodosComponent { let modalTitle: string = "Update full todo"; try { - this.todoService + this.todosApiEndpointService .updateFull(todo) .subscribe( data => { @@ -158,7 +158,7 @@ export class TodosComponent { let modalTitle: string = "Update partial todo"; try { - this.todoService + this.todosApiEndpointService .updatePartial(partialTodo) .subscribe( data => { @@ -192,7 +192,7 @@ export class TodosComponent { this.selectedTodo = undefined; let modalTitle: string = "View todo"; - this.todoService + this.todosApiEndpointService .getOne(id) .subscribe( data => { diff --git a/src/app/components/users/users.component.ts b/src/app/components/users/users.component.ts index 23b7d40..c8f4010 100644 --- a/src/app/components/users/users.component.ts +++ b/src/app/components/users/users.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { UserService } from 'src/app/services/user.service'; +import { UsersApiEndpointService } from 'src/app/services/users-api-endpoint.service'; import { UserModel } from 'src/app/models/user.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ModalComponent } from 'src/app/components/modal/modal.component'; @@ -15,7 +15,7 @@ export class UsersComponent implements OnInit { public isPerformingRequest: boolean; public selectedUser?: UserModel; - constructor(private userService: UserService, private modal: NgbModal) { + constructor(private usersApiEndpointService: UsersApiEndpointService, private modal: NgbModal) { this.users = []; this.isPerformingRequest = false; this.selectedUser = undefined; @@ -28,7 +28,7 @@ export class UsersComponent implements OnInit { private getAllUsers(): void { this.isPerformingRequest = true; - this.userService + this.usersApiEndpointService .getMany() .subscribe( data => { diff --git a/src/app/services/base-api-endpoint.service.ts b/src/app/services/base-api-endpoint.service.ts index dd4c326..ac99673 100644 --- a/src/app/services/base-api-endpoint.service.ts +++ b/src/app/services/base-api-endpoint.service.ts @@ -13,11 +13,9 @@ import { ValidationHelper } from 'src/app/helpers/validation.helper'; }) export abstract class BaseApiEndpointService> { - private pipeOperations = pipe( - map(response => { - return plainToClassFromExist(this.getInstance(), response); - }) - ); + private mapResponseToT = map(response => { + return plainToClassFromExist(this.getInstance(), response); + }); constructor(private apiService: ApiService) { } @@ -36,7 +34,7 @@ export abstract class BaseApiEndpointService> */ public create(t: T): Observable | never { return this.apiService.post(this.endpointUrl(), t) - .pipe(this.pipeOperations); + .pipe(this.mapResponseToT); } /** @@ -57,7 +55,8 @@ export abstract class BaseApiEndpointService> .pipe( map(response => { return response.map(item => plainToClassFromExist(this.getInstance(), item)); - })); + }) + ); } /** @@ -65,7 +64,7 @@ export abstract class BaseApiEndpointService> */ public getOne(id: ID): Observable { return this.apiService.getOne(this.endpointUrlWithId(id)) - .pipe(this.pipeOperations); + .pipe(this.mapResponseToT); } /** @@ -77,7 +76,7 @@ export abstract class BaseApiEndpointService> public updateFull(t: T): Observable | never { let id: ID = this.getModelId(t); return this.apiService.put(this.endpointUrlWithId(id), t) - .pipe(this.pipeOperations); + .pipe(this.mapResponseToT); } /** @@ -89,7 +88,7 @@ export abstract class BaseApiEndpointService> public updatePartial(partialT: AtLeastIdAndOneField): Observable | never { let id: ID = this.getModelId(partialT); return this.apiService.patch(this.endpointUrlWithId(id), partialT) - .pipe(this.pipeOperations); + .pipe(this.mapResponseToT); } /* private methods */ diff --git a/src/app/services/todo.service.ts b/src/app/services/todos-api-endpoint.service.ts similarity index 91% rename from src/app/services/todo.service.ts rename to src/app/services/todos-api-endpoint.service.ts index e9a25aa..e5e70c4 100644 --- a/src/app/services/todo.service.ts +++ b/src/app/services/todos-api-endpoint.service.ts @@ -8,7 +8,7 @@ import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) -export class TodoService extends BaseApiEndpointService { +export class TodosApiEndpointService extends BaseApiEndpointService { /* abstract methods from parent */ diff --git a/src/app/services/user.service.ts b/src/app/services/users-api-endpoint.service.ts similarity index 87% rename from src/app/services/user.service.ts rename to src/app/services/users-api-endpoint.service.ts index abcf141..d1f04b4 100644 --- a/src/app/services/user.service.ts +++ b/src/app/services/users-api-endpoint.service.ts @@ -6,7 +6,7 @@ import { UserModel } from 'src/app/models/user.model'; @Injectable({ providedIn: 'root' }) -export class UserService extends BaseApiEndpointService { +export class UsersApiEndpointService extends BaseApiEndpointService { /* abstract methods from parent */ From 936cfebdd9c5cb392d4ceb2e839e32aeb5582480 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 14:35:33 +0700 Subject: [PATCH 20/36] removed unused imports --- src/app/components/modal/modal.component.ts | 18 +----------------- .../todos/update-todo/update-todo.component.ts | 1 - src/app/interceptors/http-error.interceptor.ts | 2 +- src/app/services/api.service.ts | 6 +++--- src/app/services/base-api-endpoint.service.ts | 2 +- tsconfig.app.json | 3 ++- 6 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/app/components/modal/modal.component.ts b/src/app/components/modal/modal.component.ts index 085682a..7bb5a01 100644 --- a/src/app/components/modal/modal.component.ts +++ b/src/app/components/modal/modal.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ModalDismissReasons, NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-modal', @@ -20,21 +20,5 @@ export class ModalComponent { const modalRef = modal.open(ModalComponent) modalRef.componentInstance.title = title; modalRef.componentInstance.bodyLines = bodyLines; - - modalRef.result.then((res) => { - //console.log(`Closed with: ${res}`); - }, (res) => { - //console.log(`Dismissed ${ModalComponent.getDismissReason(res)}`); - }); - } - - private static getDismissReason(reason: any): string { - if (reason === ModalDismissReasons.ESC) { - return 'by pressing ESC'; - } else if (reason === ModalDismissReasons.BACKDROP_CLICK) { - return 'by clicking on a backdrop'; - } else { - return `with: ${reason}`; - } } } diff --git a/src/app/components/todos/update-todo/update-todo.component.ts b/src/app/components/todos/update-todo/update-todo.component.ts index 073672d..39276b3 100644 --- a/src/app/components/todos/update-todo/update-todo.component.ts +++ b/src/app/components/todos/update-todo/update-todo.component.ts @@ -1,7 +1,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { TodoModel } from 'src/app/models/todo.model'; import { AtLeastIdAndOneField } from 'src/app/models/base-api-endpoint.model'; -import { ValidationHelper } from 'src/app/helpers/validation.helper'; @Component({ selector: 'app-update-todo', diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index 2b402a6..d1e8b5a 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpStatusCode } from '@angular/common/http'; -import { Observable, of, pipe, throwError } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { catchError, concatMap, delay, retryWhen } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { Router } from '@angular/router'; diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 1e34b32..b5b0a9e 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; -import { Observable, pipe } from 'rxjs'; -import { delay, tap } from 'rxjs/operators'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { ValidationHelper } from 'src/app/helpers/validation.helper'; diff --git a/src/app/services/base-api-endpoint.service.ts b/src/app/services/base-api-endpoint.service.ts index ac99673..7820431 100644 --- a/src/app/services/base-api-endpoint.service.ts +++ b/src/app/services/base-api-endpoint.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable, pipe } from 'rxjs'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { AtLeastIdAndOneField, BaseApiEndpointModel, ID } from 'src/app/models/base-api-endpoint.model'; diff --git a/tsconfig.app.json b/tsconfig.app.json index 82d91dc..5e2f780 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -3,7 +3,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": [], + "noUnusedLocals": true }, "files": [ "src/main.ts", From 784494e6ed200c484c78f2684934c64b25101c57 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 19:41:19 +0700 Subject: [PATCH 21/36] updated readme description and tests --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41c077e..e2e3eb7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Angular generic API service -TODO +Angular project for a generic API service that uses [class-transformer](https://www.npmjs.com/package/class-transformer). + +The dependency packages for the demo components are: +- [angular-fontawesome](https://www.npmjs.com/package/@fortawesome/angular-fontawesome) +- [http-status-codes](https://www.npmjs.com/package/http-status-codes) +- [ng-bootstrap](https://www.npmjs.com/package/@ng-bootstrap/ng-bootstrap) + +Check out [this blog post](https://peterrhodes.dev/blog/post/angular-generic-api-service) for more details. ## Get the code @@ -63,3 +70,7 @@ Once the project has compiled successfully, open a web browser and navigate to ` > These two steps can be combined by running `ng serve -o`, which will open the app automatically in your default browser. To stop the app, go back to the terminal window and press `ctrl + C`. + +## Test + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). From b0884fbd6463df97bc8e20e85ae862ff9e0a1fd8 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 19:52:35 +0700 Subject: [PATCH 22/36] ran npm audit fix --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 554fafe..3492acc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10889,9 +10889,9 @@ "dev": true }, "tar": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.2.tgz", - "integrity": "sha512-EwKEgqJ7nJoS+s8QfLYVGMDmAsj+StbI2AM/RTHeUSsOw6Z8bwNBRv5z3CY0m7laC5qUAqruLX5AhMuc5deY3Q==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "dev": true, "requires": { "chownr": "^2.0.0", From 9e532104abe39189f81744e38bac23541a048c26 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 20:43:56 +0700 Subject: [PATCH 23/36] removed http status code import --- src/environments/environment.default.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/environments/environment.default.ts b/src/environments/environment.default.ts index c053863..f9a2765 100644 --- a/src/environments/environment.default.ts +++ b/src/environments/environment.default.ts @@ -1,5 +1,3 @@ -import { HttpStatusCode } from '@angular/common/http'; - export interface IEnvironment { production: boolean; // API @@ -20,12 +18,7 @@ export const defaultEnvironment = { API_ENDPOINT_JSON_PLACEHOLDER_USERS: 'users/', // HTTP error HTTP_ERROR_RETRY_ATTEMPTS: 3, - HTTP_ERROR_RETRY_STATUS_CODES: [ - HttpStatusCode.RequestTimeout, // 408 - HttpStatusCode.BadGateway, // 502 - HttpStatusCode.ServiceUnavailable, // 503 - HttpStatusCode.GatewayTimeout, // 504 - ], + HTTP_ERROR_RETRY_STATUS_CODES: [408, 502, 503, 504], HTTP_ERROR_RETRY_INITIAL_INTERVAL_MILLISECONDS: 1000, HTTP_ERROR_RETRY_EXPONENTIAL_DELAY_BASE: 1.5, }; From 39fd1d94db88e3adddfe74fa2df3504222299cb3 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 21:49:54 +0700 Subject: [PATCH 24/36] added angular-cli-ghpages, fixed bug in prod env --- angular.json | 6 +- package-lock.json | 145 +++++++++++++++++++++++++++ package.json | 1 + src/environments/environment.prod.ts | 2 +- 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/angular.json b/angular.json index 5d7de1c..30fb1b7 100644 --- a/angular.json +++ b/angular.json @@ -103,9 +103,13 @@ ], "scripts": [] } + }, + "deploy": { + "builder": "angular-cli-ghpages:deploy", + "options": {} } } } }, "defaultProject": "angular-generic-api-service" -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3492acc..06d6298 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2182,6 +2182,53 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "angular-cli-ghpages": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/angular-cli-ghpages/-/angular-cli-ghpages-1.0.0-rc.2.tgz", + "integrity": "sha512-oAAnu6hcNYZ5Scp1WrPUnwOHkz6JoPfwPd3b4BYifBHWlwRnB2/zXzqZRYn0vwi2aO9LqMVDbq/7dJlpAWz2LQ==", + "dev": true, + "requires": { + "commander": "^3.0.0-0", + "fs-extra": "^9.0.1", + "gh-pages": "^3.1.0" + }, + "dependencies": { + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2358,6 +2405,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -4195,6 +4248,12 @@ "integrity": "sha512-Tdx7w1fZpeWOOBluK+kXTAKCXyc79K65RB6Zp0+sPSZZhDjXlrxfGlXrlMGVVQUrKCyEZFQs1UBBLNz5IdbF0g==", "dev": true }, + "email-addresses": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", + "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4719,6 +4778,23 @@ "escape-string-regexp": "^1.0.5" } }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true + }, + "filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4984,6 +5060,57 @@ "assert-plus": "^1.0.0" } }, + "gh-pages": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-3.2.3.tgz", + "integrity": "sha512-jA1PbapQ1jqzacECfjUaO9gV8uBgU6XNMV0oXLtfCX3haGLe5Atq8BxlrADhbD6/UdG9j6tZLWAkAybndOXTJg==", + "dev": true, + "requires": { + "async": "^2.6.1", + "commander": "^2.18.0", + "email-addresses": "^3.0.1", + "filenamify": "^4.3.0", + "find-cache-dir": "^3.3.1", + "fs-extra": "^8.1.0", + "globby": "^6.1.0" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -10701,6 +10828,15 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "style-loader": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", @@ -11082,6 +11218,15 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", diff --git a/package.json b/package.json index 340cbc2..fb13fef 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@angular/compiler-cli": "~12.1.0", "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", + "angular-cli-ghpages": "^1.0.0-rc.2", "jasmine-core": "~3.7.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 4713a6c..680bf94 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,6 +1,6 @@ import { defaultEnvironment, IEnvironment } from "./environment.default"; -const env { +const env = { production: true, }; From 3f1ecf102eff18c3797b41276a6e208f8e36267e Mon Sep 17 00:00:00 2001 From: prhodesy Date: Wed, 8 Sep 2021 23:05:20 +0700 Subject: [PATCH 25/36] added example model and service --- src/app/models/example.model.ts | 17 +++++++++++++ .../services/example-api-endpoint.service.ts | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/app/models/example.model.ts create mode 100644 src/app/services/example-api-endpoint.service.ts diff --git a/src/app/models/example.model.ts b/src/app/models/example.model.ts new file mode 100644 index 0000000..e211520 --- /dev/null +++ b/src/app/models/example.model.ts @@ -0,0 +1,17 @@ +import { BaseApiEndpointModel } from 'src/app/models/base-api-endpoint.model'; +import { Type } from 'class-transformer'; + +export class ExampleModel extends BaseApiEndpointModel { + name: string = ''; + + @Type(() => NestedObject) + nestedObject: NestedObject = new NestedObject(); + + public testMethod(): string { + return 'can call this method'; + } +} + +export class NestedObject { + prop1: string = ''; +} diff --git a/src/app/services/example-api-endpoint.service.ts b/src/app/services/example-api-endpoint.service.ts new file mode 100644 index 0000000..77825ba --- /dev/null +++ b/src/app/services/example-api-endpoint.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { BaseApiEndpointService } from 'src/app/services/base-api-endpoint.service'; +import { ExampleModel } from 'src/app/models/example.model'; + +@Injectable({ + providedIn: 'root' +}) +export class ExampleApiEndpointService extends BaseApiEndpointService { + + /* abstract methods from parent */ + + public getBaseUrl(): string { + return 'https://example.com/'; + } + + public getEndpoint(): string { + return 'endpoint/'; + } + + public getInstance(): ExampleModel { + return new ExampleModel(); + } +} From 3503665b0421a0b740fb72949993e71a81ee1671 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Thu, 9 Sep 2021 11:42:58 +0700 Subject: [PATCH 26/36] added link to demo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e2e3eb7..13741ff 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ The dependency packages for the demo components are: Check out [this blog post](https://peterrhodes.dev/blog/post/angular-generic-api-service) for more details. +[DEMO](https://peterrhodesdev.github.io/angular-generic-api-service/) + ## Get the code Use one of the methods given below to get the project source code on your local machine. From 6fde5f39725832abe8e37661b9ae0b2a551507da Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 11:09:46 +0700 Subject: [PATCH 27/36] removed dependence on validation helper --- src/app/components/todos/view-todo/view-todo.component.ts | 3 +-- src/app/services/api.service.ts | 3 +-- src/app/services/base-api-endpoint.service.ts | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/components/todos/view-todo/view-todo.component.ts b/src/app/components/todos/view-todo/view-todo.component.ts index d4b18ad..69d9373 100644 --- a/src/app/components/todos/view-todo/view-todo.component.ts +++ b/src/app/components/todos/view-todo/view-todo.component.ts @@ -1,6 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { TodoModel } from 'src/app/models/todo.model'; -import { ValidationHelper } from 'src/app/helpers/validation.helper'; @Component({ selector: 'app-view-todo', @@ -20,7 +19,7 @@ export class ViewTodoComponent { } public canViewTodo(): boolean { - return !ValidationHelper.isNullOrUndefined(this.todo); + return !(this.todo === null || typeof this.todo === "undefined"); } @Output() viewTodoWithId = new EventEmitter(); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index b5b0a9e..ac36a1e 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -3,7 +3,6 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; -import { ValidationHelper } from 'src/app/helpers/validation.helper'; @Injectable({ providedIn: 'root' @@ -93,7 +92,7 @@ export class ApiService { * @throws Error If the obj argument is null or undefined. */ private convertObjToJsonString(obj: any): string | never { - if (ValidationHelper.isNullOrUndefined(obj)) { + if (obj === null || typeof obj === "undefined") { throw Error("Illegal Argument Error: can't convert null or undefined objects to JSON string"); } return JSON.stringify(obj); diff --git a/src/app/services/base-api-endpoint.service.ts b/src/app/services/base-api-endpoint.service.ts index 7820431..da76553 100644 --- a/src/app/services/base-api-endpoint.service.ts +++ b/src/app/services/base-api-endpoint.service.ts @@ -6,7 +6,6 @@ import { AtLeastIdAndOneField, BaseApiEndpointModel, ID } from 'src/app/models/b import { plainToClassFromExist } from 'class-transformer'; import { QueryParameter } from 'src/app/common/query-parameter'; import { UrlHelper } from 'src/app/helpers/url.helper'; -import { ValidationHelper } from 'src/app/helpers/validation.helper'; @Injectable({ providedIn: 'root' @@ -105,7 +104,7 @@ export abstract class BaseApiEndpointService> * @throws Error If the id field of the model is not defined. */ private getModelId(partialT: Partial): ID | never { - if (ValidationHelper.isNullOrUndefined(partialT.id)) { + if (partialT.id === null || typeof partialT.id === "undefined") { throw Error(`Illegal Argument Error: model id must be defined`); } return partialT.id!; From 8b1994fa03402cb52bb96b5543246839c1709020 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 12:13:30 +0700 Subject: [PATCH 28/36] refactored and commented --- .../interceptors/http-error.interceptor.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/app/interceptors/http-error.interceptor.ts b/src/app/interceptors/http-error.interceptor.ts index d1e8b5a..f7a97e2 100644 --- a/src/app/interceptors/http-error.interceptor.ts +++ b/src/app/interceptors/http-error.interceptor.ts @@ -8,17 +8,12 @@ import { Router } from '@angular/router'; @Injectable() export class HttpErrorInterceptor implements HttpInterceptor { - private retryAttempts: number; - private retryStatusCodes: number[]; - private retryInitialIntervalMs: number; - private retryExpDelayBase: number; + private retryAttempts: number = environment.HTTP_ERROR_RETRY_ATTEMPTS; + private retryStatusCodes: number[] = environment.HTTP_ERROR_RETRY_STATUS_CODES; + private retryInitialIntervalMs: number = environment.HTTP_ERROR_RETRY_INITIAL_INTERVAL_MILLISECONDS; + private retryExpDelayBase: number = environment.HTTP_ERROR_RETRY_EXPONENTIAL_DELAY_BASE; - constructor(private router: Router) { - this.retryAttempts = environment.HTTP_ERROR_RETRY_ATTEMPTS; - this.retryStatusCodes = environment.HTTP_ERROR_RETRY_STATUS_CODES; - this.retryInitialIntervalMs = environment.HTTP_ERROR_RETRY_INITIAL_INTERVAL_MILLISECONDS; - this.retryExpDelayBase = environment.HTTP_ERROR_RETRY_EXPONENTIAL_DELAY_BASE; - } + constructor(private router: Router) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { return next.handle(request).pipe( @@ -42,6 +37,12 @@ export class HttpErrorInterceptor implements HttpInterceptor { ); } + /** + * Calculates the delay required before the request is retried. + * The calculation uses a power function (a^b * c) to exponentially increase the length of the delay. + * @param {number} retry iteration (starting from zero) used as the exponent in the power function + * @return {number} length of time to delay in milliseconds + */ private calculateDelayMs(iteration: number): number { return Math.pow(this.retryExpDelayBase, iteration) * this.retryInitialIntervalMs; } @@ -49,29 +50,31 @@ export class HttpErrorInterceptor implements HttpInterceptor { /* handle error */ private handleError(error: HttpErrorResponse): Observable { - let errorMessage: string | undefined = this.getErrorMessage(error); + let errorMessage: string | undefined; + if(error.status === 0) { // client-side or network error + errorMessage = this.handleClientError(error); + } else { // server-side error + errorMessage = this.handleServerError(error); + } + if (!errorMessage) { // error was handled return of(error); } return throwError(errorMessage); } - private getErrorMessage(error: HttpErrorResponse): string | undefined { - if(error.status === 0) { // client-side or network error - return this.getClientNetworkErrorMessage(error); - } else { // server-side error - return this.getServerErrorMessage(error); - } - } - - private getClientNetworkErrorMessage(error: HttpErrorResponse): string { + private handleClientError(error: HttpErrorResponse): string { if (!navigator.onLine) { return 'No internet connection'; } return error.error.message; } - private getServerErrorMessage(error: HttpErrorResponse): string | undefined { + /** + * Attempt to handle the server-side error, otherwise create an error message. + * @return {string | undefined} a string with the error message, or undefined if the error was handled + */ + private handleServerError(error: HttpErrorResponse): string | undefined { let errorMessage: string | undefined; switch (error.status) { From 9eade34965819f4c0f208fcbfc400cf630b89778 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 12:44:56 +0700 Subject: [PATCH 29/36] installed qs package --- package-lock.json | 42 ++++++++++++++++++++++++++++++++++-------- package.json | 1 + 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06d6298..b134316 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2707,6 +2707,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true } } }, @@ -2846,7 +2852,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -4603,6 +4608,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true } } }, @@ -5029,7 +5040,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -5222,8 +5232,7 @@ "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-unicode": { "version": "2.0.1", @@ -7459,6 +7468,11 @@ } } }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" + }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", @@ -9575,10 +9589,12 @@ "dev": true }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } }, "querystring": { "version": "0.2.0", @@ -10343,6 +10359,16 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", diff --git a/package.json b/package.json index fb13fef..261d263 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "class-transformer": "^0.4.0", "core-js": "^3.16.4", "http-status-codes": "^2.1.4", + "qs": "^6.10.1", "rxjs": "~6.6.0", "tslib": "^2.2.0", "zone.js": "~0.11.4" From cda74519bc4a72db1b56af2d4c7bdd155b63069d Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 12:55:52 +0700 Subject: [PATCH 30/36] deleted validation helper --- src/app/helpers/url.helper.ts | 7 +- src/app/helpers/validation.helper.spec.ts | 158 ---------------------- src/app/helpers/validation.helper.ts | 36 ----- 3 files changed, 3 insertions(+), 198 deletions(-) delete mode 100644 src/app/helpers/validation.helper.spec.ts delete mode 100644 src/app/helpers/validation.helper.ts diff --git a/src/app/helpers/url.helper.ts b/src/app/helpers/url.helper.ts index bbe5e7a..8b0da61 100644 --- a/src/app/helpers/url.helper.ts +++ b/src/app/helpers/url.helper.ts @@ -1,5 +1,4 @@ import { QueryParameter } from 'src/app/common/query-parameter'; -import { ValidationHelper } from 'src/app/helpers/validation.helper'; export abstract class UrlHelper { @@ -13,15 +12,15 @@ export abstract class UrlHelper { public static getValidQueryString(params: QueryParameter[]): string { // Filter out empty names - let validParams: QueryParameter[] = params.filter(element => !ValidationHelper.isEmpty(element.name)); - if (!ValidationHelper.hasElements(validParams)) { + let validParams: QueryParameter[] = params.filter(element => element.name !== ''); + if (validParams.length === 0) { return ''; } let queryStringElements: string[] = []; for (let param of validParams) { let element: string = encodeURIComponent(param.name); - if (ValidationHelper.hasNonEmptyValue(param.value)) { + if (param.value !== null && typeof param.value !== 'undefined' && param.value !== '') { element += '=' + encodeURIComponent(param.value!); } queryStringElements.push(element); diff --git a/src/app/helpers/validation.helper.spec.ts b/src/app/helpers/validation.helper.spec.ts deleted file mode 100644 index 61573a2..0000000 --- a/src/app/helpers/validation.helper.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { ValidationHelper } from './validation.helper'; - -describe('ValidationHelper', () => { - it('#isNull should return true for null variable', () => { - // Arrange - let nullObject: any = null; - - // Act - let result = ValidationHelper.isNull(nullObject); - - // Assert - expect(result).toBeTruthy(); - }); - - it('#isNull should return false for non-null variables', () => { - // Arrange - let undefinedObject: any = undefined; - let emptyObject: any = {}; - let emptyString: string = ''; - let falseBoolean: boolean = false; - let zeroNumber: number = 0; - let nanNumber: number = NaN; - - // Act - let undefinedObjectResult = ValidationHelper.isNull(undefinedObject); - let emptyObjectResult = ValidationHelper.isNull(emptyObject); - let emptyStringResult = ValidationHelper.isNull(emptyString); - let falseBooleanResult = ValidationHelper.isNull(falseBoolean); - let zeroNumberResult = ValidationHelper.isNull(zeroNumber); - let nanNumberResult = ValidationHelper.isNull(nanNumber); - - // Assert - expect(undefinedObjectResult).toBeFalsy(); - expect(emptyObjectResult).toBeFalsy(); - expect(emptyStringResult).toBeFalsy(); - expect(falseBooleanResult).toBeFalsy(); - expect(zeroNumberResult).toBeFalsy(); - expect(nanNumberResult).toBeFalsy(); - }); - - it('#isUndefined should return true for undefined variable', () => { - // Arrange - let undefinedObject: any = undefined; - - // Act - let result = ValidationHelper.isUndefined(undefinedObject); - - // Assert - expect(result).toBeTruthy(); - }); - - it('#isUndefined should return false for non-undefined variables', () => { - // Arrange - let nullObject: any = null; - let emptyObject: any = {}; - let emptyString: string = ''; - let falseBoolean: boolean = false; - let zeroNumber: number = 0; - let nanNumber: number = NaN; - - // Act - let nullObjectResult = ValidationHelper.isUndefined(nullObject); - let emptyObjectResult = ValidationHelper.isUndefined(emptyObject); - let emptyStringResult = ValidationHelper.isUndefined(emptyString); - let falseBooleanResult = ValidationHelper.isUndefined(falseBoolean); - let zeroNumberResult = ValidationHelper.isUndefined(zeroNumber); - let nanNumberResult = ValidationHelper.isUndefined(nanNumber); - - // Assert - expect(nullObjectResult).toBeFalsy(); - expect(emptyObjectResult).toBeFalsy(); - expect(emptyStringResult).toBeFalsy(); - expect(falseBooleanResult).toBeFalsy(); - expect(zeroNumberResult).toBeFalsy(); - expect(nanNumberResult).toBeFalsy(); - }); - - it('#isEmpty should return true for empty string', () => { - // Arrange - let emptyString: string = ''; - - // Act - let result = ValidationHelper.isEmpty(emptyString); - - // Assert - expect(result).toBeTruthy(); - }); - - it('#isEmpty should return false for non-empty strings', () => { - // Arrange - let spaceString: string = ' '; - let singleCharacterString: string = 'a'; - let multipleCharacterString: string = 'abc'; - - // Act - let spaceStringResult = ValidationHelper.isEmpty(spaceString); - let singleCharacterStringResult = ValidationHelper.isEmpty(singleCharacterString); - let multipleCharacterStringResult = ValidationHelper.isEmpty(multipleCharacterString); - - // Assert - expect(spaceStringResult).toBeFalsy(); - expect(singleCharacterStringResult).toBeFalsy(); - expect(multipleCharacterStringResult).toBeFalsy(); - }); - - it('#isArrayEmpty should return true for empty array', () => { - // Arrange - let emptyArray: any[] = []; - - // Act - let result = ValidationHelper.isArrayEmpty(emptyArray); - - // Assert - expect(result).toBeTruthy(); - }); - - it('#isArrayEmpty should return false for non-empty array', () => { - // Arrange - let nonEmptyArray: any[] = [ {} ]; - - // Act - let result = ValidationHelper.isArrayEmpty(nonEmptyArray); - - // Assert - expect(result).toBeFalsy(); - }); - - it('#isString should return false for non-strings', () => { - // Arrange - let num: number = 0; - let bool: boolean = false; - let arr: string[] = []; - let obj: any = {}; - - // Act - let numResult = ValidationHelper.isString(num); - let boolResult = ValidationHelper.isString(bool); - let arrResult = ValidationHelper.isString(arr); - let objResult = ValidationHelper.isString(obj); - - // Assert - expect(numResult).toBeFalsy(); - expect(boolResult).toBeFalsy(); - expect(arrResult).toBeFalsy(); - expect(objResult).toBeFalsy(); - }); - - it('#isString should return true for string', () => { - // Arrange - let str: string = ''; - - // Act - let result = ValidationHelper.isString(str); - - // Assert - expect(result).toBeTruthy(); - }); -}); diff --git a/src/app/helpers/validation.helper.ts b/src/app/helpers/validation.helper.ts deleted file mode 100644 index 164d1d0..0000000 --- a/src/app/helpers/validation.helper.ts +++ /dev/null @@ -1,36 +0,0 @@ -export abstract class ValidationHelper { - - public static isNull(t?: T): boolean { - return t === null; - } - - public static isUndefined(t?: T): boolean { - return typeof t === "undefined"; - } - - public static isNullOrUndefined(t?: T): boolean { - return ValidationHelper.isNull(t) || ValidationHelper.isUndefined(t); - } - - public static isEmpty(str: string): boolean { - return str === ""; - } - - // String not empty, null, or undefined - public static hasNonEmptyValue(str?: string): boolean { - return !ValidationHelper.isNullOrUndefined(str) && !ValidationHelper.isEmpty(str!); - } - - public static isArrayEmpty(t: T[]): boolean { - return t.length === 0; - } - - // Array not empty, null, or undefined - public static hasElements(t?: T[]): boolean { - return !ValidationHelper.isNullOrUndefined(t) && !ValidationHelper.isArrayEmpty(t!); - } - - public static isString(t: T): boolean { - return typeof t === 'string' || t instanceof String; - } -} From 3fd24fb793f351e4892e53b7422669d07ddd5ac6 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 13:12:28 +0700 Subject: [PATCH 31/36] installed @types/qs, replaced query string logic with qs --- package-lock.json | 5 +++++ package.json | 1 + src/app/helpers/url.helper.ts | 11 +++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b134316..6424e54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1886,6 +1886,11 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", diff --git a/package.json b/package.json index 261d263..6edee76 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/free-brands-svg-icons": "^5.15.3", "@ng-bootstrap/ng-bootstrap": "^10.0.0", + "@types/qs": "^6.9.7", "bootstrap": "^4.5.0", "class-transformer": "^0.4.0", "core-js": "^3.16.4", diff --git a/src/app/helpers/url.helper.ts b/src/app/helpers/url.helper.ts index 8b0da61..529c7ff 100644 --- a/src/app/helpers/url.helper.ts +++ b/src/app/helpers/url.helper.ts @@ -1,4 +1,5 @@ import { QueryParameter } from 'src/app/common/query-parameter'; +import { stringify } from 'qs'; export abstract class UrlHelper { @@ -17,6 +18,12 @@ export abstract class UrlHelper { return ''; } + let queryObj: any = {}; + for (let param of validParams) { + queryObj[param.name] = param.value; + } + + /* let queryStringElements: string[] = []; for (let param of validParams) { let element: string = encodeURIComponent(param.name); @@ -24,9 +31,9 @@ export abstract class UrlHelper { element += '=' + encodeURIComponent(param.value!); } queryStringElements.push(element); - } + }*/ - let queryString: string = '?' + queryStringElements.join('&'); + let queryString: string = '?' + stringify(queryObj); return UrlHelper.isQueryStringValid(queryString) ? queryString : ''; } } From d5ae20904760a47ad4449160337267423ad9bfb4 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 13:24:33 +0700 Subject: [PATCH 32/36] undefined filtered out by qs --- src/app/common/query-parameter.ts | 4 +- src/app/helpers/url.helper.spec.ts | 68 ------------------------------ 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/src/app/common/query-parameter.ts b/src/app/common/query-parameter.ts index e7ecad9..067fbd0 100644 --- a/src/app/common/query-parameter.ts +++ b/src/app/common/query-parameter.ts @@ -1,8 +1,8 @@ export class QueryParameter { name: string; - value?: string; + value: string; - constructor(name: string, value?: string) { + constructor(name: string, value: string) { this.name = name; this.value = value; } diff --git a/src/app/helpers/url.helper.spec.ts b/src/app/helpers/url.helper.spec.ts index bc480c5..31396f6 100644 --- a/src/app/helpers/url.helper.spec.ts +++ b/src/app/helpers/url.helper.spec.ts @@ -54,72 +54,4 @@ describe('UrlHelper', () => { // Assert expect(result).toBeTruthy(); }); - - it('#getValidQueryString should return an empty string when there are no params', () => { - // Arrange - let params: QueryParameter[] = []; - - // Act - let paramsResult = UrlHelper.getValidQueryString(params); - - // Assert - let emptyString: string = ''; - expect(paramsResult).toEqual(emptyString); - }); - - it('#getValidQueryString should filter out empty names', () => { - // Arrange - let emptyString: string = ''; - let params: QueryParameter[] = [ - new QueryParameter(emptyString, 'value'), - ]; - - // Act - let paramsResult = UrlHelper.getValidQueryString(params); - - // Assert - expect(paramsResult).toEqual(emptyString); - }); - - it('#getValidQueryString should return string for single param', () => { - // Arrange - let params: QueryParameter[] = [ - new QueryParameter('name', 'value'), - ]; - - // Act - let paramsResult = UrlHelper.getValidQueryString(params); - - // Assert - expect(paramsResult).toEqual('?name=value'); - }); - - it('#getValidQueryString should return string with escaped reserved characters', () => { - // Arrange - let params: QueryParameter[] = [ - new QueryParameter('name', 'value with spaces'), - ]; - - // Act - let paramsResult = UrlHelper.getValidQueryString(params); - - // Assert - expect(paramsResult).toEqual('?name=value%20with%20spaces'); - }); - - it('#getValidQueryString should return string for multiple params', () => { - // Arrange - let params: QueryParameter[] = [ - new QueryParameter('name1', 'value1'), - new QueryParameter('name2', ''), - new QueryParameter('name3', undefined), - new QueryParameter('name4', 'value2'), - ]; - - // Act - let paramsResult = UrlHelper.getValidQueryString(params); - - // Assert - expect(paramsResult).toEqual('?name1=value1&name2&name3&name4=value2'); - }); }); From 257d0d4569a60b0a016621fb8a9ba1f83deea703 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 13:35:40 +0700 Subject: [PATCH 33/36] deleted query param class and replaced with query obj --- src/app/common/query-parameter.ts | 9 ------ src/app/helpers/url.helper.spec.ts | 1 - src/app/helpers/url.helper.ts | 28 ++----------------- src/app/services/base-api-endpoint.service.ts | 5 ++-- .../services/todos-api-endpoint.service.ts | 7 +++-- 5 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 src/app/common/query-parameter.ts diff --git a/src/app/common/query-parameter.ts b/src/app/common/query-parameter.ts deleted file mode 100644 index 067fbd0..0000000 --- a/src/app/common/query-parameter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class QueryParameter { - name: string; - value: string; - - constructor(name: string, value: string) { - this.name = name; - this.value = value; - } -} diff --git a/src/app/helpers/url.helper.spec.ts b/src/app/helpers/url.helper.spec.ts index 31396f6..4c51c7a 100644 --- a/src/app/helpers/url.helper.spec.ts +++ b/src/app/helpers/url.helper.spec.ts @@ -1,5 +1,4 @@ import { UrlHelper } from './url.helper'; -import { QueryParameter } from 'src/app/common/query-parameter'; describe('UrlHelper', () => { diff --git a/src/app/helpers/url.helper.ts b/src/app/helpers/url.helper.ts index 529c7ff..09f846e 100644 --- a/src/app/helpers/url.helper.ts +++ b/src/app/helpers/url.helper.ts @@ -1,4 +1,3 @@ -import { QueryParameter } from 'src/app/common/query-parameter'; import { stringify } from 'qs'; export abstract class UrlHelper { @@ -11,29 +10,8 @@ export abstract class UrlHelper { return pattern.test(queryString); } - public static getValidQueryString(params: QueryParameter[]): string { - // Filter out empty names - let validParams: QueryParameter[] = params.filter(element => element.name !== ''); - if (validParams.length === 0) { - return ''; - } - - let queryObj: any = {}; - for (let param of validParams) { - queryObj[param.name] = param.value; - } - - /* - let queryStringElements: string[] = []; - for (let param of validParams) { - let element: string = encodeURIComponent(param.name); - if (param.value !== null && typeof param.value !== 'undefined' && param.value !== '') { - element += '=' + encodeURIComponent(param.value!); - } - queryStringElements.push(element); - }*/ - - let queryString: string = '?' + stringify(queryObj); - return UrlHelper.isQueryStringValid(queryString) ? queryString : ''; + public static createQueryString(queryObj: any): string { + let queryString: string = stringify(queryObj); + return queryString !== '' ? '?' + queryString : ''; } } diff --git a/src/app/services/base-api-endpoint.service.ts b/src/app/services/base-api-endpoint.service.ts index da76553..5066fb1 100644 --- a/src/app/services/base-api-endpoint.service.ts +++ b/src/app/services/base-api-endpoint.service.ts @@ -4,7 +4,6 @@ import { map } from 'rxjs/operators'; import { ApiService } from 'src/app/services/api.service'; import { AtLeastIdAndOneField, BaseApiEndpointModel, ID } from 'src/app/models/base-api-endpoint.model'; import { plainToClassFromExist } from 'class-transformer'; -import { QueryParameter } from 'src/app/common/query-parameter'; import { UrlHelper } from 'src/app/helpers/url.helper'; @Injectable({ @@ -46,8 +45,8 @@ export abstract class BaseApiEndpointService> /** * Get multiple models. */ - public getMany(params: QueryParameter[] = []): Observable { - let queryString: string = UrlHelper.getValidQueryString(params); + public getMany(queryObj: any = {}): Observable { + let queryString: string = UrlHelper.createQueryString(queryObj); let requestUrl: string = this.endpointUrl() + queryString; return this.apiService.getMany(requestUrl) diff --git a/src/app/services/todos-api-endpoint.service.ts b/src/app/services/todos-api-endpoint.service.ts index e5e70c4..1381044 100644 --- a/src/app/services/todos-api-endpoint.service.ts +++ b/src/app/services/todos-api-endpoint.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { BaseApiEndpointService } from 'src/app/services/base-api-endpoint.service'; import { TodoModel } from 'src/app/models/todo.model'; -import { QueryParameter } from 'src/app/common/query-parameter'; import { Observable } from 'rxjs'; @Injectable({ @@ -27,7 +26,9 @@ export class TodosApiEndpointService extends BaseApiEndpointService { /* public methods */ public getManyFilterByUserId(userId: number): Observable { - let param: QueryParameter = new QueryParameter("userId", userId.toString()); - return this.getMany([ param ]); + let queryObj: any = { + userId: userId.toString(), + }; + return this.getMany(queryObj); } } From f787d4a8e3313e60888d2d962818bd98ae2546eb Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 13:39:12 +0700 Subject: [PATCH 34/36] allowed qs commonjs dependency --- angular.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/angular.json b/angular.json index 30fb1b7..91ee18a 100644 --- a/angular.json +++ b/angular.json @@ -33,7 +33,10 @@ "styles": [ "src/styles.scss" ], - "scripts": [] + "scripts": [], + "allowedCommonJsDependencies": [ + "qs" + ] }, "configurations": { "production": { @@ -112,4 +115,4 @@ } }, "defaultProject": "angular-generic-api-service" -} \ No newline at end of file +} From 4a69ddff36c99cb51f431e196daa5e4f7c5b3655 Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 13:41:55 +0700 Subject: [PATCH 35/36] renamed qs stringify --- src/app/helpers/url.helper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/helpers/url.helper.ts b/src/app/helpers/url.helper.ts index 09f846e..6dd71b8 100644 --- a/src/app/helpers/url.helper.ts +++ b/src/app/helpers/url.helper.ts @@ -1,4 +1,4 @@ -import { stringify } from 'qs'; +import { stringify as qs_stringify } from 'qs'; export abstract class UrlHelper { @@ -11,7 +11,7 @@ export abstract class UrlHelper { } public static createQueryString(queryObj: any): string { - let queryString: string = stringify(queryObj); + let queryString: string = qs_stringify(queryObj); return queryString !== '' ? '?' + queryString : ''; } } From 8a92f68a885bba1629d3fbab1a451ea3b80b548f Mon Sep 17 00:00:00 2001 From: prhodesy Date: Sat, 11 Sep 2021 19:04:40 +0700 Subject: [PATCH 36/36] moved examples and created example component --- src/app/app.module.ts | 4 +- src/app/examples/example.component.ts | 115 ++++++++++++++++++ src/app/{models => examples}/example.model.ts | 10 +- .../example.service.ts} | 5 +- 4 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 src/app/examples/example.component.ts rename src/app/{models => examples}/example.model.ts (55%) rename src/app/{services/example-api-endpoint.service.ts => examples/example.service.ts} (66%) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9e35326..7b6ac42 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -22,6 +22,7 @@ import { ModalComponent } from './components/modal/modal.component'; import { HttpErrorInterceptor } from './interceptors/http-error.interceptor'; import { HttpStatusCodesComponent } from './components/http-status-codes/http-status-codes.component'; import { LoginComponent } from './components/login/login.component'; +import { ExampleComponent } from './examples/example.component'; @NgModule({ declarations: [ @@ -39,7 +40,8 @@ import { LoginComponent } from './components/login/login.component'; TodoFormComponent, ModalComponent, HttpStatusCodesComponent, - LoginComponent + LoginComponent, + ExampleComponent ], imports: [ BrowserModule, diff --git a/src/app/examples/example.component.ts b/src/app/examples/example.component.ts new file mode 100644 index 0000000..e40dcf2 --- /dev/null +++ b/src/app/examples/example.component.ts @@ -0,0 +1,115 @@ +import { Component } from '@angular/core'; +import { ExampleService } from 'src/app/examples/example.service'; +import { ExampleModel } from 'src/app/examples/example.model'; +import { AtLeastIdAndOneField } from 'src/app/models/base-api-endpoint.model'; + +@Component({ + selector: 'app-example', + template: ``, + styles: [''] +}) +export class ExampleComponent { + + public example?: ExampleModel = undefined; + public examples: ExampleModel[] = []; + + constructor(private exampleService: ExampleService) {} + + public performRequests(): void { + // Create + this.createExample(new ExampleModel()); + // Delete + this.deleteExample(1); + // Get + this.getManyExamples(); + this.getOneExample(1); + // Update + let modifiedExample: ExampleModel = new ExampleModel(); + modifiedExample.id = 1; + this.updateFullExample(modifiedExample); + let partialExample: AtLeastIdAndOneField = { id: 1, strProp: '', }; + this.updatePartialExample(partialExample); + } + + /* create */ + + private createExample(newExample: ExampleModel): void { + try { + this.exampleService.create(newExample) + .subscribe( + data => { this.examples.push(data); }, + error => { console.log(`Error creating example: ${error}`); } + ) + .add(() => { console.log(`Finished request`); }); + } catch(error) { + console.log(`Error trying to create example: ${error}`); + } + } + + /* delete */ + + private deleteExample(id: number): void { + this.exampleService.delete(id) + .subscribe( + data => { this.examples = this.examples.filter(example => example.id !== id); }, + error => { console.log(`Error deleting example with id = ${id}: ${error}`); } + ) + .add(() => { console.log(`Finished request`); }); + } + + /* get */ + + private getManyExamples(): void { + this.exampleService.getMany() + .subscribe( + data => { this.examples = [...data]; }, + error => { console.log(`Error getting examples: ${error}`); } + ) + .add(() => { console.log(`Finished request`); }); + } + + private getOneExample(id: number): void { + this.exampleService.getOne(id) + .subscribe( + data => { this.example = data; }, + error => { console.log(`Error getting example with id = ${id}: ${error}`); } + ) + .add(() => { console.log(`Finished request`); }); + } + + /* update */ + + private updateFullExample(modifiedExample: ExampleModel): void { + try { + this.exampleService.updateFull(modifiedExample) + .subscribe( + data => { this.exampleUpdated(data); }, + error => { console.log(`Error updating example: ${error}`); } + ) + .add(() => { console.log(`Finished request`); }); + } catch(error) { + console.log(`Error trying to update example: ${error}`); + } + } + + private updatePartialExample(partialExample: AtLeastIdAndOneField): void { + try { + this.exampleService.updatePartial(partialExample) + .subscribe( + data => { this.exampleUpdated(data); }, + error => { console.log(`Error updating example: ${error}`); } + ) + .add(() => { console.log(`Finished request`); }); + } catch(error) { + console.log(`Error trying to update example: ${error}`); + } + } + + private exampleUpdated(updatedExample: ExampleModel): void { + this.examples.forEach(example => { + if (example.id === updatedExample.id) { + example = updatedExample; + } + }); + } +} diff --git a/src/app/models/example.model.ts b/src/app/examples/example.model.ts similarity index 55% rename from src/app/models/example.model.ts rename to src/app/examples/example.model.ts index e211520..0f0ade0 100644 --- a/src/app/models/example.model.ts +++ b/src/app/examples/example.model.ts @@ -2,13 +2,15 @@ import { BaseApiEndpointModel } from 'src/app/models/base-api-endpoint.model'; import { Type } from 'class-transformer'; export class ExampleModel extends BaseApiEndpointModel { - name: string = ''; + strProp: string = ''; + arrProp: string[] = []; + optProp?: string = undefined; @Type(() => NestedObject) - nestedObject: NestedObject = new NestedObject(); + nestedObj: NestedObject = new NestedObject(); - public testMethod(): string { - return 'can call this method'; + public exampleMethod(): string { + return 'can call this method after transformation'; } } diff --git a/src/app/services/example-api-endpoint.service.ts b/src/app/examples/example.service.ts similarity index 66% rename from src/app/services/example-api-endpoint.service.ts rename to src/app/examples/example.service.ts index 77825ba..b811605 100644 --- a/src/app/services/example-api-endpoint.service.ts +++ b/src/app/examples/example.service.ts @@ -1,12 +1,11 @@ import { Injectable } from '@angular/core'; -import { environment } from 'src/environments/environment'; import { BaseApiEndpointService } from 'src/app/services/base-api-endpoint.service'; -import { ExampleModel } from 'src/app/models/example.model'; +import { ExampleModel } from 'src/app/examples/example.model'; @Injectable({ providedIn: 'root' }) -export class ExampleApiEndpointService extends BaseApiEndpointService { +export class ExampleService extends BaseApiEndpointService { /* abstract methods from parent */