simplify models, and datastore

- example on static functions and TS generics
- exmaple on data encapsulation and "single responsibility"
This commit is contained in:
Sean Engelhardt 2018-07-03 11:52:16 +02:00 committed by FinnStutzenstein
parent 8b31fa15f2
commit 2b60b4ef4f
9 changed files with 8784 additions and 106 deletions

View File

@ -37,6 +37,7 @@ import { StartComponent } from './site/start/start.component';
import { ToastComponent } from './core/directives/toast/toast.component'; import { ToastComponent } from './core/directives/toast/toast.component';
import { ToastService } from './core/services/toast.service'; import { ToastService } from './core/services/toast.service';
import { WebsocketService } from './core/services/websocket.service'; import { WebsocketService } from './core/services/websocket.service';
import { DS } from './core/services/DS.service';
import { ProjectorContainerComponent } from './projector-container/projector-container.component'; import { ProjectorContainerComponent } from './projector-container/projector-container.component';
import { AlertComponent } from './core/directives/alert/alert.component'; import { AlertComponent } from './core/directives/alert/alert.component';
@ -95,7 +96,7 @@ library.add(fas);
}), }),
AppRoutingModule AppRoutingModule
], ],
providers: [Title, ToastService, WebsocketService], providers: [Title, ToastService, WebsocketService, DS],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,48 +1,42 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DS } from 'app/core/services/DS.service'; // import { DS } from 'app/core/services/DS.service';
import { ImproperlyConfiguredError } from 'app/core/exceptions';
const INVALID_COLLECTION_STRING = 'invalid-collection-string'; const INVALID_COLLECTION_STRING = 'invalid-collection-string';
export type ModelId = number | string; export type ModelId = number | string;
export abstract class BaseModel { export abstract class BaseModel {
abstract id: ModelId; id: ModelId;
constructor() {} constructor() {}
// Typescript does not allow static and abstract at the same time :((( // convert an serialized version of the model to an instance of the class
static getCollectionString(): string { // jsonString is usually the server respince
// T is the target model, User, Motion, Whatever
// demands full functionening Models with constructors
static fromJSON(jsonString: {}, T): BaseModel {
// create an instance of the User class
const model = Object.create(T.prototype);
// copy all the fields from the json object
return Object.assign(model, jsonString);
}
//hast to be overwritten by the children.
//Could be more generic: e.g. a model-enum
public getCollectionString(): string {
return INVALID_COLLECTION_STRING; return INVALID_COLLECTION_STRING;
} }
private static getCheckedCollectionString(): string { //TODO document this function.
public getCheckedCollectionString(): string {
const collectionString: string = this.getCollectionString(); const collectionString: string = this.getCollectionString();
if (collectionString === INVALID_COLLECTION_STRING) { if (collectionString === INVALID_COLLECTION_STRING) {
throw new ImproperlyConfigured( throw new ImproperlyConfiguredError(
'Invalid collection string: Please override the static getCollectionString method!' 'Invalid collection string: Please override the static getCollectionString method!'
); );
} }
return collectionString; return collectionString;
} }
protected static _get<T extends BaseModel>(id: ModelId): T | undefined {
return DS.get(this.getCheckedCollectionString(), id) as T;
}
protected static _getAll<T extends BaseModel>(): T[] {
return DS.getAll(this.getCheckedCollectionString()) as T[];
}
protected static _filter<T extends BaseModel>(callback): T[] {
return DS.filter(this.getCheckedCollectionString(), callback) as T[];
}
abstract getCollectionString(): string;
save(): Observable<any> {
return DS.save(this);
}
delete(): Observable<any> {
return DS.delete(this);
}
} }

View File

@ -3,24 +3,13 @@ import { BaseModel } from './baseModel';
export class Group extends BaseModel { export class Group extends BaseModel {
id: number; id: number;
name: string; name: string;
permissions: string[]; permissions: string[]; //TODO permissions could be an own model?
constructor(id: number) { constructor(id: number, name?: string, permissions?: string[]) {
super(); super();
this.id = id; this.id = id;
} this.name = name;
this.permissions = permissions;
static getCollectionString(): string {
return 'users/group';
}
static get(id: number): Group | undefined {
return this._get<Group>(id);
}
static getAll(): Group[] {
return this._getAll<Group>();
}
static filter(callback): Group[] {
return this._filter<Group>(callback);
} }
getCollectionString(): string { getCollectionString(): string {

View File

@ -1,6 +1,9 @@
import { BaseModel } from './baseModel'; import { BaseModel } from './baseModel';
// import { DS } from 'app/core/services/DS.service';
export class User extends BaseModel { export class User extends BaseModel {
//TODO potentially make them private and use getters and setters
id: number; id: number;
username: string; username: string;
title: string; title: string;
@ -18,29 +21,53 @@ export class User extends BaseModel {
is_active: boolean; is_active: boolean;
default_password: string; default_password: string;
constructor(id: number) { //default constructer with every possible optinal parameter for conventient usage
constructor(
id: number,
username?: string,
title?: string,
first_name?: string,
last_name?: string,
structure_level?: string,
number?: string,
about_me?: string,
groups_id?: number[],
is_present?: boolean,
is_committee?: boolean,
email?: string,
last_email_send?: string,
comment?: string,
is_active?: boolean,
default_password?: string
) {
super(); super();
this.id = id; this.id = id;
} this.username = username;
this.title = title;
static getCollectionString(): string { this.first_name = first_name;
return 'users/user'; this.last_name = last_name;
} this.structure_level = structure_level;
// Make this static lookup methods typesafe this.number = number;
// TODO: I'm not happy about this: this.about_me = about_me;
// - this has to be done for every model this.groups_id = groups_id;
// - this may be extendet, if there are more static functionallities for models. this.is_present = is_present;
static get(id: number): User | undefined { this.is_committee = is_committee;
return this._get<User>(id); this.email = email;
} this.last_email_send = last_email_send;
static getAll(): User[] { this.comment = comment;
return this._getAll<User>(); this.is_active = is_active;
} this.default_password = default_password;
static filter(callback): User[] {
return this._filter<User>(callback);
} }
getCollectionString(): string { getCollectionString(): string {
return 'users/user'; return 'users/user';
} }
// // convert an serialized version of the User to an instance of the class
// static fromJSON(jsonString: {}): User {
// // create an instance of the User class
// let user = Object.create(User.prototype);
// // copy all the fields from the json object
// return Object.assign(user, jsonString);
// }
} }

View File

@ -1,4 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ImproperlyConfiguredError } from 'app/core/exceptions'; import { ImproperlyConfiguredError } from 'app/core/exceptions';
import { BaseModel, ModelId } from 'app/core/models/baseModel'; import { BaseModel, ModelId } from 'app/core/models/baseModel';
@ -11,80 +14,110 @@ interface DataStore {
[collectionString: string]: Collection; [collectionString: string]: Collection;
} }
//Todo: DRY. This is a copy from /authService. probably repository service necessary
const httpOptions = {
withCredentials: true,
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
@Injectable({
providedIn: 'root'
})
export class DS { export class DS {
static DS: DataStore; private store: DataStore = {};
private constructor() {} // Just a static class! constructor(private http: HttpClient) {}
static get(collectionString: string, id: ModelId): BaseModel | undefined { get(collectionString: string, id: ModelId): BaseModel | undefined {
const collection: Collection = DS[collectionString]; const collection: Collection = this.store[collectionString];
if (!collection) { if (!collection) {
return; return;
} }
const model: BaseModel = collection[id]; const model: BaseModel = collection[id];
return model; return model;
} }
static getAll(collectionString: string): BaseModel[] {
const collection: Collection = DS[collectionString]; //todo return observable of base model
getAll(collectionString: string): BaseModel[] {
const collection: Collection = this.store[collectionString];
if (!collection) { if (!collection) {
return []; return [];
} }
return Object.values(collection); return Object.values(collection);
} }
// TODO: type for callback function // TODO: type for callback function
static filter(collectionString: string, callback): BaseModel[] { filter(collectionString: string, callback): BaseModel[] {
return this.getAll(collectionString).filter(callback); return this.getAll(collectionString).filter(callback);
} }
static inject(collectionString: string, model: BaseModel): void { inject(model: BaseModel): void {
const collectionString = model.getCollectionString();
console.log('the collection string: ', collectionString);
if (!model.id) { if (!model.id) {
throw new ImproperlyConfiguredError('The model must have an id!'); throw new ImproperlyConfiguredError('The model must have an id!');
} } else if (collectionString === 'invalid-collection-string') {
if (model.getCollectionString() !== collectionString) { throw new ImproperlyConfiguredError('Cannot save a BaseModel');
throw new ImproperlyConfiguredError('The model you try to insert has not the right collection string');
}
if (!DS[collectionString]) {
DS[collectionString] = {};
}
DS[collectionString][model.id] = model;
} }
static injectMany(collectionString: string, models: BaseModel[]): void { if (typeof this.store[collectionString] === 'undefined') {
this.store[collectionString] = {};
console.log('made new collection: ', collectionString);
}
this.store[collectionString][model.id] = model;
console.log('injected ; ', model);
}
injectMany(models: BaseModel[]): void {
models.forEach(model => { models.forEach(model => {
DS.inject(collectionString, model); this.inject(model);
}); });
} }
static eject(collectionString: string, id: ModelId) { eject(collectionString: string, id: ModelId) {
if (DS[collectionString]) { if (this.store[collectionString]) {
delete DS[collectionString][id]; delete this.store[collectionString][id];
} }
} }
static ejectMany(collectionString: string, ids: ModelId[]) {
ejectMany(collectionString: string, ids: ModelId[]) {
ids.forEach(id => { ids.forEach(id => {
DS.eject(collectionString, id); this.eject(collectionString, id);
}); });
} }
// TODO remove the any there and in BaseModel. // TODO remove the any there and in BaseModel.
static save(model: BaseModel): Observable<any> { save(model: BaseModel): Observable<BaseModel> {
if (!model.id) { if (!model.id) {
throw new ImproperlyConfiguredError('The model must have an id!'); throw new ImproperlyConfiguredError('The model must have an id!');
} }
const collectionString: string = model.getCollectionString(); const collectionString: string = model.getCollectionString();
// make http request to the server
// if this was a success, inject the model into the DS //TODO not tested
return of(); return this.http.post<BaseModel>(collectionString + '/', model, httpOptions).pipe(
tap(response => {
console.log('the response: ', response);
this.inject(model);
})
);
} }
// TODO remove the any there and in BaseModel. // TODO remove the any there and in BaseModel.
static delete(model: BaseModel): Observable<any> { delete(model: BaseModel): Observable<any> {
if (!model.id) { if (!model.id) {
throw new ImproperlyConfiguredError('The model must have an id!'); throw new ImproperlyConfiguredError('The model must have an id!');
} }
const collectionString: string = model.getCollectionString(); const collectionString: string = model.getCollectionString();
// make http request to the server
// if this was a success, eject the model from the DS //TODO not tested
return of(); return this.http.post<BaseModel>(collectionString + '/', model, httpOptions).pipe(
tap(response => {
console.log('the response: ', response);
this.eject(collectionString, model.id);
})
);
} }
} }

View File

@ -48,12 +48,10 @@ export class AuthService {
username: username, username: username,
password: password password: password
}; };
return this.http.post<User>('/users/login/', user, httpOptions).pipe( return this.http.post<any>('/users/login/', user, httpOptions).pipe(
tap(val => { tap(val => {
localStorage.setItem('username', val.username); localStorage.setItem('username', val.username);
this.isLoggedIn = true; this.isLoggedIn = true;
//Set the session cookie in local storrage.
//TODO needs validation
}), }),
catchError(this.handleError()) catchError(this.handleError())
); );

View File

@ -9,6 +9,12 @@ import { tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; //showcase import { TranslateService } from '@ngx-translate/core'; //showcase
//into own service
import { DS } from 'app/core/services/DS.service';
import { User } from 'app/core/models/user';
import { Group } from 'app/core/models/group';
import { BaseModel } from '../core/models/baseModel';
@Component({ @Component({
selector: 'app-site', selector: 'app-site',
templateUrl: './site.component.html', templateUrl: './site.component.html',
@ -22,7 +28,8 @@ export class SiteComponent implements OnInit {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private router: Router, private router: Router,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private translate: TranslateService private translate: TranslateService,
private dS: DS
) {} ) {}
ngOnInit() { ngOnInit() {
@ -42,6 +49,7 @@ export class SiteComponent implements OnInit {
// subscribe to the socket // subscribe to the socket
socket.subscribe(response => { socket.subscribe(response => {
console.log('log : ', response); // will contain all the config variables console.log('log : ', response); // will contain all the config variables
this.storeResponse(response);
}); });
// basically everything needed for AutoUpdate // basically everything needed for AutoUpdate
@ -55,6 +63,27 @@ export class SiteComponent implements OnInit {
}); });
} }
//test. will move to an own service later
//create models out of socket answer
storeResponse(socketResponse): void {
socketResponse.forEach(model => {
switch (model.collection) {
case 'users/group': {
this.dS.inject(BaseModel.fromJSON(model.data, Group));
break;
}
case 'users/user': {
this.dS.inject(BaseModel.fromJSON(model.data, User));
break;
}
default: {
console.log('collection: "' + model.collection + '" is not yet parsed');
break;
}
}
});
}
selectLang(lang: string): void { selectLang(lang: string): void {
console.log('selected langauge: ', lang); console.log('selected langauge: ', lang);
console.log('get Langs : ', this.translate.getLangs()); console.log('get Langs : ', this.translate.getLangs());

View File

@ -12,8 +12,11 @@ import { DS } from 'app/core/services/DS.service';
styleUrls: ['./start.component.css'] styleUrls: ['./start.component.css']
}) })
export class StartComponent extends BaseComponent implements OnInit { export class StartComponent extends BaseComponent implements OnInit {
constructor(titleService: Title) { private dS: DS;
constructor(titleService: Title, dS: DS) {
super(titleService); super(titleService);
this.dS = dS;
} }
ngOnInit() { ngOnInit() {
@ -22,19 +25,29 @@ export class StartComponent extends BaseComponent implements OnInit {
test() { test() {
// This can be a basic unit test ;) // This can be a basic unit test ;)
console.log(User.get(1)); // console.log(User.get(1));
const user1: User = new User(1); const user1: User = new User(32, 'testuser');
user1.username = 'testuser'; const user2: User = new User(42, 'testuser 2');
const user2: User = new User(2);
user2.username = 'testuser2';
DS.injectMany(User.getCollectionString(), [user1, user2]); console.log(`User1 | ID ${user1.id}, Name: ${user1.username}`);
console.log(User.getAll()); console.log(`User2 | ID ${user2.id}, Name: ${user2.username}`);
console.log(User.filter(user => user.id === 1));
DS.eject(User.getCollectionString(), user1.id); this.dS.inject(user1);
console.log(User.getAll()); this.dS.inject(user2);
console.log(User.filter(user => user.id === 1)); console.log('All users = ', this.dS.getAll('users/user'));
console.log(User.filter(user => user.id === 2));
console.log('try to get user with ID 1:');
const user1fromStore = this.dS.get('users/user', 1);
console.log('the user: ', user1fromStore);
console.log('inject many:');
this.dS.injectMany([user1, user2]);
console.log('eject user 1');
this.dS.eject('users/user', user1.id);
console.log(this.dS.getAll('users/user'));
// console.log(User.filter(user => user.id === 1));
// console.log(User.filter(user => user.id === 2));
} }
} }

8594
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff