Client: centralize http res extraction in a service
authorChocobozzz <florian.bigard@gmail.com>
Tue, 23 Aug 2016 14:54:21 +0000 (16:54 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Tue, 23 Aug 2016 14:54:21 +0000 (16:54 +0200)
17 files changed:
client/src/app/account/account.service.ts
client/src/app/admin/friends/shared/friend.service.ts
client/src/app/admin/users/shared/user.service.ts
client/src/app/app.component.ts
client/src/app/login/login.component.ts
client/src/app/shared/auth/auth.service.ts
client/src/app/shared/index.ts
client/src/app/shared/rest/index.ts [new file with mode: 0644]
client/src/app/shared/rest/rest-extractor.service.ts [new file with mode: 0644]
client/src/app/shared/rest/rest-pagination.ts [new file with mode: 0644]
client/src/app/shared/rest/rest.service.ts [new file with mode: 0644]
client/src/app/videos/shared/index.ts
client/src/app/videos/shared/pagination.model.ts [deleted file]
client/src/app/videos/shared/video.service.ts
client/src/app/videos/video-list/video-list.component.ts
client/src/main.ts
client/tsconfig.json

index 19b4e06244680f0dda73827c3c6e5d65af9126ec..355bcef742ebd9fffca1cefb27bd440e3551133f 100644 (file)
@@ -1,12 +1,16 @@
 import { Injectable } from '@angular/core';
 
-import { AuthHttp, AuthService } from '../shared';
+import { AuthHttp, AuthService, RestExtractor } from '../shared';
 
 @Injectable()
 export class AccountService {
   private static BASE_USERS_URL = '/api/v1/users/';
 
-  constructor(private authHttp: AuthHttp, private authService: AuthService) {  }
+  constructor(
+    private authHttp: AuthHttp,
+    private authService: AuthService,
+    private restExtractor: RestExtractor
+  ) {}
 
   changePassword(newPassword: string) {
     const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
@@ -14,6 +18,8 @@ export class AccountService {
       password: newPassword
     };
 
-    return this.authHttp.put(url, body);
+    return this.authHttp.put(url, body)
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 }
index e4e680c29179623d67d5a5d6af529886c41a5864..75826fc17bccac2e7c2388d7997a5aa42db4e1fa 100644 (file)
@@ -1,9 +1,8 @@
 import { Injectable } from '@angular/core';
-import { Response } from '@angular/http';
 import { Observable } from 'rxjs/Observable';
 
 import { Friend } from './friend.model';
-import { AuthHttp, AuthService } from '../../../shared';
+import { AuthHttp, RestExtractor } from '../../../shared';
 
 @Injectable()
 export class FriendService {
@@ -11,13 +10,15 @@ export class FriendService {
 
   constructor (
     private authHttp: AuthHttp,
-    private authService: AuthService
+    private restExtractor: RestExtractor
   ) {}
 
   getFriends(): Observable<Friend[]> {
     return this.authHttp.get(FriendService.BASE_FRIEND_URL)
-                        .map(res => <Friend[]>res.json())
-                        .catch(this.handleError);
+                        // Not implemented as a data list by the server yet
+                        // .map(this.restExtractor.extractDataList)
+                        .map((res) => res.json())
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 
   makeFriends(notEmptyUrls) {
@@ -26,18 +27,13 @@ export class FriendService {
     };
 
     return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
-                        .map(res => res.status)
-                        .catch(this.handleError);
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 
   quitFriends() {
     return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
                         .map(res => res.status)
-                        .catch(this.handleError);
-  }
-
-  private handleError (error: Response) {
-    console.error(error);
-    return Observable.throw(error.json().error || 'Server error');
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 }
index be433f0a12113dc4e0111c9ec12e8be077445b64..d96db4575d8638cdf643d6d9043236fca91b7616 100644 (file)
@@ -1,15 +1,16 @@
 import { Injectable } from '@angular/core';
-import { Response } from '@angular/http';
-import { Observable } from 'rxjs/Observable';
 
-import { AuthHttp, User } from '../../../shared';
+import { AuthHttp, RestExtractor, ResultList, User } from '../../../shared';
 
 @Injectable()
 export class UserService {
   // TODO: merge this constant with account
   private static BASE_USERS_URL = '/api/v1/users/';
 
-  constructor(private authHttp: AuthHttp) {}
+  constructor(
+    private authHttp: AuthHttp,
+    private restExtractor: RestExtractor
+  ) {}
 
   addUser(username: string, password: string) {
     const body = {
@@ -17,23 +18,25 @@ export class UserService {
       password
     };
 
-    return this.authHttp.post(UserService.BASE_USERS_URL, body);
+    return this.authHttp.post(UserService.BASE_USERS_URL, body)
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 
   getUsers() {
     return this.authHttp.get(UserService.BASE_USERS_URL)
-                 .map(res => res.json())
+                 .map(this.restExtractor.extractDataList)
                  .map(this.extractUsers)
-                 .catch(this.handleError);
+                 .catch((res) => this.restExtractor.handleError(res));
   }
 
   removeUser(user: User) {
     return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
   }
 
-  private extractUsers(body: any) {
-    const usersJson = body.data;
-    const totalUsers = body.total;
+  private extractUsers(result: ResultList) {
+    const usersJson = result.data;
+    const totalUsers = result.total;
     const users = [];
     for (const userJson of usersJson) {
       users.push(new User(userJson));
@@ -41,9 +44,4 @@ export class UserService {
 
     return { users, totalUsers };
   }
-
-  private handleError(error: Response) {
-    console.error(error);
-    return Observable.throw(error.json().error || 'Server error');
-  }
 }
index 2e0fd13f1e49075f7c3b3451b1b2a2bc1d6cfed1..9d05c272ffc1f47c05f28a78bcafa2fa5a7df62b 100644 (file)
@@ -3,7 +3,7 @@ import { Router, ROUTER_DIRECTIVES } from '@angular/router';
 
 import { MenuAdminComponent } from './admin';
 import { MenuComponent } from './menu.component';
-import { SearchComponent, SearchService } from './shared';
+import { RestExtractor, RestService, SearchComponent, SearchService } from './shared';
 import { VideoService } from './videos';
 
 @Component({
@@ -11,7 +11,7 @@ import { VideoService } from './videos';
     template: require('./app.component.html'),
     styles: [ require('./app.component.scss') ],
     directives: [ MenuAdminComponent, MenuComponent, ROUTER_DIRECTIVES, SearchComponent ],
-    providers: [ VideoService, SearchService ]
+    providers: [ RestExtractor, RestService, VideoService, SearchService ]
 })
 
 export class AppComponent {
index fe867b7b435761dd4758c6a4a0df11ae9a64e30e..1e0ba0fe8c2a666538c870c552a64b1ab472deba 100644 (file)
@@ -37,12 +37,12 @@ export class LoginComponent implements OnInit {
         this.router.navigate(['/videos/list']);
       },
       error => {
-        console.error(error);
+        console.error(error.json);
 
-        if (error.error === 'invalid_grant') {
+        if (error.json.error === 'invalid_grant') {
           this.error = 'Credentials are invalid.';
         } else {
-          this.error = `${error.error}: ${error.error_description}`;
+          this.error = `${error.json.error}: ${error.json.error_description}`;
         }
       }
     );
index 8eea0c4bfed4f812c9cc97e85618243c0e06d610..2273048c88026c2527c9195fd1f19c2d096d0b6c 100644 (file)
@@ -1,10 +1,11 @@
 import { Injectable } from '@angular/core';
-import { Headers, Http, Response, URLSearchParams } from '@angular/http';
+import { Headers, Http, URLSearchParams } from '@angular/http';
 import { Observable } from 'rxjs/Observable';
 import { Subject } from 'rxjs/Subject';
 
 import { AuthStatus } from './auth-status.model';
 import { AuthUser } from './auth-user.model';
+import { RestExtractor } from '../rest';
 
 @Injectable()
 export class AuthService {
@@ -19,15 +20,15 @@ export class AuthService {
   private loginChanged: Subject<AuthStatus>;
   private user: AuthUser = null;
 
-  constructor(private http: Http) {
+  constructor(private http: Http, private restExtractor: RestExtractor) {
     this.loginChanged = new Subject<AuthStatus>();
     this.loginChangedSource = this.loginChanged.asObservable();
 
     // Fetch the client_id/client_secret
     // FIXME: save in local storage?
     this.http.get(AuthService.BASE_CLIENT_URL)
-      .map(res => res.json())
-      .catch(this.handleError)
+      .map(this.restExtractor.extractDataGet)
+      .catch((res) => this.restExtractor.handleError(res))
       .subscribe(
         result => {
           this.clientId = result.client_id;
@@ -101,14 +102,14 @@ export class AuthService {
     };
 
     return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
-                    .map(res => res.json())
+                    .map(this.restExtractor.extractDataGet)
                     .map(res => {
                       res.username = username;
                       return res;
                     })
                     .flatMap(res => this.fetchUserInformations(res))
                     .map(res => this.handleLogin(res))
-                    .catch(this.handleError);
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
   logout() {
@@ -139,9 +140,9 @@ export class AuthService {
     };
 
     return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
-                    .map(res => res.json())
+                    .map(this.restExtractor.extractDataGet)
                     .map(res => this.handleRefreshToken(res))
-                    .catch(this.handleError);
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
   private fetchUserInformations (obj: any) {
@@ -160,11 +161,6 @@ export class AuthService {
     );
   }
 
-  private handleError (error: Response) {
-    console.error(error);
-    return Observable.throw(error.json() || { error: 'Server error' });
-  }
-
   private handleLogin (obj: any) {
     const id = obj.id;
     const username = obj.username;
index 9edf9b4a0a713791446181ab948ecd312ce09a75..c362a0e4ae00690a933f1f9362ae69d694144e21 100644 (file)
@@ -1,4 +1,5 @@
 export * from './auth';
 export * from './form-validators';
+export * from './rest';
 export * from './search';
 export * from './users';
diff --git a/client/src/app/shared/rest/index.ts b/client/src/app/shared/rest/index.ts
new file mode 100644 (file)
index 0000000..3c9509d
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './rest-extractor.service';
+export * from './rest-pagination';
+export * from './rest.service';
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts
new file mode 100644 (file)
index 0000000..aa44799
--- /dev/null
@@ -0,0 +1,46 @@
+import { Injectable } from '@angular/core';
+import { Response } from '@angular/http';
+import { Observable } from 'rxjs/Observable';
+
+export interface ResultList {
+  data: any[];
+  total: number;
+}
+
+@Injectable()
+export class RestExtractor {
+
+  constructor () { ; }
+
+  extractDataBool(res: Response) {
+    return true;
+  }
+
+  extractDataList(res: Response) {
+    const body = res.json();
+
+    const ret: ResultList = {
+      data: body.data,
+      total: body.total
+    };
+
+    return ret;
+  }
+
+  extractDataGet(res: Response) {
+    return res.json();
+  }
+
+  handleError(res: Response) {
+    let text = 'Server error: ';
+    text += res.text();
+    let json = res.json();
+
+    const error = {
+      json,
+      text
+    };
+
+    return Observable.throw(error);
+  }
+}
diff --git a/client/src/app/shared/rest/rest-pagination.ts b/client/src/app/shared/rest/rest-pagination.ts
new file mode 100644 (file)
index 0000000..0cfa4f4
--- /dev/null
@@ -0,0 +1,5 @@
+export interface RestPagination {
+  currentPage: number;
+  itemsPerPage: number;
+  totalItems: number;
+};
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts
new file mode 100644 (file)
index 0000000..16b47e9
--- /dev/null
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import { URLSearchParams } from '@angular/http';
+
+import { RestPagination } from './rest-pagination';
+
+@Injectable()
+export class RestService {
+
+  buildRestGetParams(pagination?: RestPagination, sort?: string) {
+    const params = new URLSearchParams();
+
+    if (pagination) {
+      const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
+      const count: number = pagination.itemsPerPage;
+
+      params.set('start', start.toString());
+      params.set('count', count.toString());
+    }
+
+    if (sort) {
+      params.set('sort', sort);
+    }
+
+    return params;
+  }
+
+}
index a54120f5d5d2c69a8a1e8f7a4523e138bf6e1e66..67d16ead155876bf31f47d54dc653fc3faa3cab6 100644 (file)
@@ -1,5 +1,4 @@
 export * from './loader';
-export * from './pagination.model';
 export * from './sort-field.type';
 export * from './video.model';
 export * from './video.service';
diff --git a/client/src/app/videos/shared/pagination.model.ts b/client/src/app/videos/shared/pagination.model.ts
deleted file mode 100644 (file)
index eda44eb..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface Pagination {
-  currentPage: number;
-  itemsPerPage: number;
-  totalItems: number;
-}
index b4396f76794abe1eebc2265e66959f3f6ce9f08d..ad855753344a2f289a2713bdda9f46c8b5b62062 100644 (file)
@@ -1,11 +1,10 @@
 import { Injectable } from '@angular/core';
-import { Http, Response, URLSearchParams } from '@angular/http';
+import { Http } from '@angular/http';
 import { Observable } from 'rxjs/Observable';
 
-import { Pagination } from './pagination.model';
 import { Search } from '../../shared';
 import { SortField } from './sort-field.type';
-import { AuthHttp, AuthService } from '../../shared';
+import { AuthHttp, AuthService, RestExtractor, RestPagination, RestService, ResultList } from '../../shared';
 import { Video } from './video.model';
 
 @Injectable()
@@ -15,68 +14,51 @@ export class VideoService {
   constructor(
     private authService: AuthService,
     private authHttp: AuthHttp,
-    private http: Http
+    private http: Http,
+    private restExtractor: RestExtractor,
+    private restService: RestService
   ) {}
 
-  getVideo(id: string) {
+  getVideo(id: string): Observable<Video> {
     return this.http.get(VideoService.BASE_VIDEO_URL + id)
-                    .map(res => <Video> res.json())
-                    .catch(this.handleError);
+                    .map(this.restExtractor.extractDataGet)
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
-  getVideos(pagination: Pagination, sort: SortField) {
-    const params = this.createPaginationParams(pagination);
-
-    if (sort) params.set('sort', sort);
+  getVideos(pagination: RestPagination, sort: SortField) {
+    const params = this.restService.buildRestGetParams(pagination, sort);
 
     return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
                     .map(res => res.json())
                     .map(this.extractVideos)
-                    .catch(this.handleError);
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
   removeVideo(id: string) {
     return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
-                        .map(res => <number> res.status)
-                        .catch(this.handleError);
+                        .map(this.restExtractor.extractDataBool)
+                        .catch((res) => this.restExtractor.handleError(res));
   }
 
-  searchVideos(search: Search, pagination: Pagination, sort: SortField) {
-    const params = this.createPaginationParams(pagination);
+  searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
+    const params = this.restService.buildRestGetParams(pagination, sort);
 
     if (search.field) params.set('field', search.field);
-    if (sort) params.set('sort', sort);
 
     return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
-                    .map(res => res.json())
+                    .map(this.restExtractor.extractDataList)
                     .map(this.extractVideos)
-                    .catch(this.handleError);
-  }
-
-  private createPaginationParams(pagination: Pagination) {
-    const params = new URLSearchParams();
-    const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage;
-    const count: number = pagination.itemsPerPage;
-
-    params.set('start', start.toString());
-    params.set('count', count.toString());
-
-    return params;
+                    .catch((res) => this.restExtractor.handleError(res));
   }
 
-  private extractVideos(body: any) {
-    const videos_json = body.data;
-    const totalVideos = body.total;
+  private extractVideos(result: ResultList) {
+    const videosJson = result.data;
+    const totalVideos = result.total;
     const videos = [];
-    for (const video_json of videos_json) {
-      videos.push(new Video(video_json));
+    for (const videoJson of videosJson) {
+      videos.push(new Video(videoJson));
     }
 
     return { videos, totalVideos };
   }
-
-  private handleError(error: Response) {
-    console.error(error);
-    return Observable.throw(error.json().error || 'Server error');
-  }
 }
index 7c6d4b992d387463136502c05b85a81e51fe73c9..1324a6214f0f364df364308529665c3aeea2cd92 100644 (file)
@@ -7,12 +7,11 @@ import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
 
 import {
   LoaderComponent,
-  Pagination,
   SortField,
   Video,
   VideoService
 } from '../shared';
-import { AuthService, AuthUser, Search, SearchField } from '../../shared';
+import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
 import { VideoMiniatureComponent } from './video-miniature.component';
 import { VideoSortComponent } from './video-sort.component';
 import { SearchService } from '../../shared';
@@ -27,7 +26,7 @@ import { SearchService } from '../../shared';
 
 export class VideoListComponent implements OnInit, OnDestroy {
   loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
-  pagination: Pagination = {
+  pagination: RestPagination = {
     currentPage: 1,
     itemsPerPage: 9,
     totalItems: null
index 7c058e12fd165a77e993f602a1c23feb8f3857c0..7caabe9141f38005127418feeb8505b6e3ca9b24 100644 (file)
@@ -9,7 +9,7 @@ import { bootstrap }    from '@angular/platform-browser-dynamic';
 import { provideRouter } from '@angular/router';
 
 import { routes } from './app/app.routes';
-import { AuthHttp, AuthService } from './app/shared';
+import { AuthHttp, AuthService, RestExtractor } from './app/shared';
 import { AppComponent } from './app/app.component';
 
 if (process.env.ENV === 'production') {
@@ -26,6 +26,7 @@ bootstrap(AppComponent, [
   }),
 
   AuthService,
+  RestExtractor,
 
   provideRouter(routes),
 
index 53e6fd5716c3380b6d37d370a5d555e83d0d6739..60ca1422195167f883ad5c41a641b92b97133de2 100644 (file)
     "src/app/shared/form-validators/index.ts",
     "src/app/shared/form-validators/url.validator.ts",
     "src/app/shared/index.ts",
+    "src/app/shared/rest/index.ts",
+    "src/app/shared/rest/mock-rest-table.ts",
+    "src/app/shared/rest/rest-extractor.service.ts",
+    "src/app/shared/rest/rest-filter.model.ts",
+    "src/app/shared/rest/rest-pagination.ts",
+    "src/app/shared/rest/rest-sort.ts",
+    "src/app/shared/rest/rest-table-page.ts",
+    "src/app/shared/rest/rest-table.spec.ts",
+    "src/app/shared/rest/rest-table.ts",
+    "src/app/shared/rest/rest.service.ts",
     "src/app/shared/search/index.ts",
     "src/app/shared/search/search-field.type.ts",
     "src/app/shared/search/search.component.ts",