Client: centralize http res extraction in a service

This commit is contained in:
Chocobozzz 2016-08-23 16:54:21 +02:00
parent def16d33d1
commit de59c48f5f
16 changed files with 160 additions and 96 deletions

View File

@ -1,12 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AuthHttp, AuthService } from '../shared'; import { AuthHttp, AuthService, RestExtractor } from '../shared';
@Injectable() @Injectable()
export class AccountService { export class AccountService {
private static BASE_USERS_URL = '/api/v1/users/'; 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) { changePassword(newPassword: string) {
const url = AccountService.BASE_USERS_URL + this.authService.getUser().id; const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
@ -14,6 +18,8 @@ export class AccountService {
password: newPassword password: newPassword
}; };
return this.authHttp.put(url, body); return this.authHttp.put(url, body)
.map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res));
} }
} }

View File

@ -1,9 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Response } from '@angular/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Friend } from './friend.model'; import { Friend } from './friend.model';
import { AuthHttp, AuthService } from '../../../shared'; import { AuthHttp, RestExtractor } from '../../../shared';
@Injectable() @Injectable()
export class FriendService { export class FriendService {
@ -11,13 +10,15 @@ export class FriendService {
constructor ( constructor (
private authHttp: AuthHttp, private authHttp: AuthHttp,
private authService: AuthService private restExtractor: RestExtractor
) {} ) {}
getFriends(): Observable<Friend[]> { getFriends(): Observable<Friend[]> {
return this.authHttp.get(FriendService.BASE_FRIEND_URL) return this.authHttp.get(FriendService.BASE_FRIEND_URL)
.map(res => <Friend[]>res.json()) // Not implemented as a data list by the server yet
.catch(this.handleError); // .map(this.restExtractor.extractDataList)
.map((res) => res.json())
.catch((res) => this.restExtractor.handleError(res));
} }
makeFriends(notEmptyUrls) { makeFriends(notEmptyUrls) {
@ -26,18 +27,13 @@ export class FriendService {
}; };
return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body) return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'makefriends', body)
.map(res => res.status) .map(this.restExtractor.extractDataBool)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
quitFriends() { quitFriends() {
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends') return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quitfriends')
.map(res => res.status) .map(res => res.status)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
}
private handleError (error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
} }
} }

View File

@ -1,15 +1,16 @@
import { Injectable } from '@angular/core'; 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() @Injectable()
export class UserService { export class UserService {
// TODO: merge this constant with account // TODO: merge this constant with account
private static BASE_USERS_URL = '/api/v1/users/'; private static BASE_USERS_URL = '/api/v1/users/';
constructor(private authHttp: AuthHttp) {} constructor(
private authHttp: AuthHttp,
private restExtractor: RestExtractor
) {}
addUser(username: string, password: string) { addUser(username: string, password: string) {
const body = { const body = {
@ -17,23 +18,25 @@ export class UserService {
password 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() { getUsers() {
return this.authHttp.get(UserService.BASE_USERS_URL) return this.authHttp.get(UserService.BASE_USERS_URL)
.map(res => res.json()) .map(this.restExtractor.extractDataList)
.map(this.extractUsers) .map(this.extractUsers)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
removeUser(user: User) { removeUser(user: User) {
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id); return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
} }
private extractUsers(body: any) { private extractUsers(result: ResultList) {
const usersJson = body.data; const usersJson = result.data;
const totalUsers = body.total; const totalUsers = result.total;
const users = []; const users = [];
for (const userJson of usersJson) { for (const userJson of usersJson) {
users.push(new User(userJson)); users.push(new User(userJson));
@ -41,9 +44,4 @@ export class UserService {
return { users, totalUsers }; return { users, totalUsers };
} }
private handleError(error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
} }

View File

@ -3,7 +3,7 @@ import { Router, ROUTER_DIRECTIVES } from '@angular/router';
import { MenuAdminComponent } from './admin'; import { MenuAdminComponent } from './admin';
import { MenuComponent } from './menu.component'; import { MenuComponent } from './menu.component';
import { SearchComponent, SearchService } from './shared'; import { RestExtractor, RestService, SearchComponent, SearchService } from './shared';
import { VideoService } from './videos'; import { VideoService } from './videos';
@Component({ @Component({
@ -11,7 +11,7 @@ import { VideoService } from './videos';
template: require('./app.component.html'), template: require('./app.component.html'),
styles: [ require('./app.component.scss') ], styles: [ require('./app.component.scss') ],
directives: [ MenuAdminComponent, MenuComponent, ROUTER_DIRECTIVES, SearchComponent ], directives: [ MenuAdminComponent, MenuComponent, ROUTER_DIRECTIVES, SearchComponent ],
providers: [ VideoService, SearchService ] providers: [ RestExtractor, RestService, VideoService, SearchService ]
}) })
export class AppComponent { export class AppComponent {

View File

@ -37,12 +37,12 @@ export class LoginComponent implements OnInit {
this.router.navigate(['/videos/list']); this.router.navigate(['/videos/list']);
}, },
error => { error => {
console.error(error); console.error(error.json);
if (error.error === 'invalid_grant') { if (error.json.error === 'invalid_grant') {
this.error = 'Credentials are invalid.'; this.error = 'Credentials are invalid.';
} else { } else {
this.error = `${error.error}: ${error.error_description}`; this.error = `${error.json.error}: ${error.json.error_description}`;
} }
} }
); );

View File

@ -1,10 +1,11 @@
import { Injectable } from '@angular/core'; 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 { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject'; import { Subject } from 'rxjs/Subject';
import { AuthStatus } from './auth-status.model'; import { AuthStatus } from './auth-status.model';
import { AuthUser } from './auth-user.model'; import { AuthUser } from './auth-user.model';
import { RestExtractor } from '../rest';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@ -19,15 +20,15 @@ export class AuthService {
private loginChanged: Subject<AuthStatus>; private loginChanged: Subject<AuthStatus>;
private user: AuthUser = null; private user: AuthUser = null;
constructor(private http: Http) { constructor(private http: Http, private restExtractor: RestExtractor) {
this.loginChanged = new Subject<AuthStatus>(); this.loginChanged = new Subject<AuthStatus>();
this.loginChangedSource = this.loginChanged.asObservable(); this.loginChangedSource = this.loginChanged.asObservable();
// Fetch the client_id/client_secret // Fetch the client_id/client_secret
// FIXME: save in local storage? // FIXME: save in local storage?
this.http.get(AuthService.BASE_CLIENT_URL) this.http.get(AuthService.BASE_CLIENT_URL)
.map(res => res.json()) .map(this.restExtractor.extractDataGet)
.catch(this.handleError) .catch((res) => this.restExtractor.handleError(res))
.subscribe( .subscribe(
result => { result => {
this.clientId = result.client_id; this.clientId = result.client_id;
@ -101,14 +102,14 @@ export class AuthService {
}; };
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
.map(res => res.json()) .map(this.restExtractor.extractDataGet)
.map(res => { .map(res => {
res.username = username; res.username = username;
return res; return res;
}) })
.flatMap(res => this.fetchUserInformations(res)) .flatMap(res => this.fetchUserInformations(res))
.map(res => this.handleLogin(res)) .map(res => this.handleLogin(res))
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
logout() { logout() {
@ -139,9 +140,9 @@ export class AuthService {
}; };
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
.map(res => res.json()) .map(this.restExtractor.extractDataGet)
.map(res => this.handleRefreshToken(res)) .map(res => this.handleRefreshToken(res))
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
private fetchUserInformations (obj: any) { 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) { private handleLogin (obj: any) {
const id = obj.id; const id = obj.id;
const username = obj.username; const username = obj.username;

View File

@ -1,4 +1,5 @@
export * from './auth'; export * from './auth';
export * from './form-validators'; export * from './form-validators';
export * from './rest';
export * from './search'; export * from './search';
export * from './users'; export * from './users';

View File

@ -0,0 +1,3 @@
export * from './rest-extractor.service';
export * from './rest-pagination';
export * from './rest.service';

View File

@ -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);
}
}

View File

@ -1,5 +1,5 @@
export interface Pagination { export interface RestPagination {
currentPage: number; currentPage: number;
itemsPerPage: number; itemsPerPage: number;
totalItems: number; totalItems: number;
} };

View File

@ -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;
}
}

View File

@ -1,5 +1,4 @@
export * from './loader'; export * from './loader';
export * from './pagination.model';
export * from './sort-field.type'; export * from './sort-field.type';
export * from './video.model'; export * from './video.model';
export * from './video.service'; export * from './video.service';

View File

@ -1,11 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Http, Response, URLSearchParams } from '@angular/http'; import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Pagination } from './pagination.model';
import { Search } from '../../shared'; import { Search } from '../../shared';
import { SortField } from './sort-field.type'; 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'; import { Video } from './video.model';
@Injectable() @Injectable()
@ -15,68 +14,51 @@ export class VideoService {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
private authHttp: AuthHttp, 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) return this.http.get(VideoService.BASE_VIDEO_URL + id)
.map(res => <Video> res.json()) .map(this.restExtractor.extractDataGet)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
getVideos(pagination: Pagination, sort: SortField) { getVideos(pagination: RestPagination, sort: SortField) {
const params = this.createPaginationParams(pagination); const params = this.restService.buildRestGetParams(pagination, sort);
if (sort) params.set('sort', sort);
return this.http.get(VideoService.BASE_VIDEO_URL, { search: params }) return this.http.get(VideoService.BASE_VIDEO_URL, { search: params })
.map(res => res.json()) .map(res => res.json())
.map(this.extractVideos) .map(this.extractVideos)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
removeVideo(id: string) { removeVideo(id: string) {
return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id) return this.authHttp.delete(VideoService.BASE_VIDEO_URL + id)
.map(res => <number> res.status) .map(this.restExtractor.extractDataBool)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
searchVideos(search: Search, pagination: Pagination, sort: SortField) { searchVideos(search: Search, pagination: RestPagination, sort: SortField) {
const params = this.createPaginationParams(pagination); const params = this.restService.buildRestGetParams(pagination, sort);
if (search.field) params.set('field', search.field); 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 }) 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) .map(this.extractVideos)
.catch(this.handleError); .catch((res) => this.restExtractor.handleError(res));
} }
private createPaginationParams(pagination: Pagination) { private extractVideos(result: ResultList) {
const params = new URLSearchParams(); const videosJson = result.data;
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; const totalVideos = result.total;
const count: number = pagination.itemsPerPage;
params.set('start', start.toString());
params.set('count', count.toString());
return params;
}
private extractVideos(body: any) {
const videos_json = body.data;
const totalVideos = body.total;
const videos = []; const videos = [];
for (const video_json of videos_json) { for (const videoJson of videosJson) {
videos.push(new Video(video_json)); videos.push(new Video(videoJson));
} }
return { videos, totalVideos }; return { videos, totalVideos };
} }
private handleError(error: Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
} }

View File

@ -7,12 +7,11 @@ import { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/components/pagination';
import { import {
LoaderComponent, LoaderComponent,
Pagination,
SortField, SortField,
Video, Video,
VideoService VideoService
} from '../shared'; } from '../shared';
import { AuthService, AuthUser, Search, SearchField } from '../../shared'; import { AuthService, AuthUser, RestPagination, Search, SearchField } from '../../shared';
import { VideoMiniatureComponent } from './video-miniature.component'; import { VideoMiniatureComponent } from './video-miniature.component';
import { VideoSortComponent } from './video-sort.component'; import { VideoSortComponent } from './video-sort.component';
import { SearchService } from '../../shared'; import { SearchService } from '../../shared';
@ -27,7 +26,7 @@ import { SearchService } from '../../shared';
export class VideoListComponent implements OnInit, OnDestroy { export class VideoListComponent implements OnInit, OnDestroy {
loading: BehaviorSubject<boolean> = new BehaviorSubject(false); loading: BehaviorSubject<boolean> = new BehaviorSubject(false);
pagination: Pagination = { pagination: RestPagination = {
currentPage: 1, currentPage: 1,
itemsPerPage: 9, itemsPerPage: 9,
totalItems: null totalItems: null

View File

@ -9,7 +9,7 @@ import { bootstrap } from '@angular/platform-browser-dynamic';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app/app.routes'; 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'; import { AppComponent } from './app/app.component';
if (process.env.ENV === 'production') { if (process.env.ENV === 'production') {
@ -26,6 +26,7 @@ bootstrap(AppComponent, [
}), }),
AuthService, AuthService,
RestExtractor,
provideRouter(routes), provideRouter(routes),

View File

@ -68,6 +68,16 @@
"src/app/shared/form-validators/index.ts", "src/app/shared/form-validators/index.ts",
"src/app/shared/form-validators/url.validator.ts", "src/app/shared/form-validators/url.validator.ts",
"src/app/shared/index.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/index.ts",
"src/app/shared/search/search-field.type.ts", "src/app/shared/search/search-field.type.ts",
"src/app/shared/search/search.component.ts", "src/app/shared/search/search.component.ts",