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 { ToastService } from './core/services/toast.service';
import { WebsocketService } from './core/services/websocket.service';
import { DS } from './core/services/DS.service';
import { ProjectorContainerComponent } from './projector-container/projector-container.component';
import { AlertComponent } from './core/directives/alert/alert.component';
@ -95,7 +96,7 @@ library.add(fas);
}),
AppRoutingModule
],
providers: [Title, ToastService, WebsocketService],
providers: [Title, ToastService, WebsocketService, DS],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@ -1,48 +1,42 @@
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';
export type ModelId = number | string;
export abstract class BaseModel {
abstract id: ModelId;
id: ModelId;
constructor() {}
// Typescript does not allow static and abstract at the same time :(((
static getCollectionString(): string {
// convert an serialized version of the model to an instance of the class
// 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;
}
private static getCheckedCollectionString(): string {
//TODO document this function.
public getCheckedCollectionString(): string {
const collectionString: string = this.getCollectionString();
if (collectionString === INVALID_COLLECTION_STRING) {
throw new ImproperlyConfigured(
throw new ImproperlyConfiguredError(
'Invalid collection string: Please override the static getCollectionString method!'
);
}
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 {
id: number;
name: string;
permissions: string[];
permissions: string[]; //TODO permissions could be an own model?
constructor(id: number) {
constructor(id: number, name?: string, permissions?: string[]) {
super();
this.id = id;
}
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);
this.name = name;
this.permissions = permissions;
}
getCollectionString(): string {

View File

@ -1,6 +1,9 @@
import { BaseModel } from './baseModel';
// import { DS } from 'app/core/services/DS.service';
export class User extends BaseModel {
//TODO potentially make them private and use getters and setters
id: number;
username: string;
title: string;
@ -18,29 +21,53 @@ export class User extends BaseModel {
is_active: boolean;
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();
this.id = id;
}
static getCollectionString(): string {
return 'users/user';
}
// Make this static lookup methods typesafe
// TODO: I'm not happy about this:
// - this has to be done for every model
// - this may be extendet, if there are more static functionallities for models.
static get(id: number): User | undefined {
return this._get<User>(id);
}
static getAll(): User[] {
return this._getAll<User>();
}
static filter(callback): User[] {
return this._filter<User>(callback);
this.username = username;
this.title = title;
this.first_name = first_name;
this.last_name = last_name;
this.structure_level = structure_level;
this.number = number;
this.about_me = about_me;
this.groups_id = groups_id;
this.is_present = is_present;
this.is_committee = is_committee;
this.email = email;
this.last_email_send = last_email_send;
this.comment = comment;
this.is_active = is_active;
this.default_password = default_password;
}
getCollectionString(): string {
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 { tap } from 'rxjs/operators';
import { ImproperlyConfiguredError } from 'app/core/exceptions';
import { BaseModel, ModelId } from 'app/core/models/baseModel';
@ -11,80 +14,110 @@ interface DataStore {
[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 {
static DS: DataStore;
private store: DataStore = {};
private constructor() {} // Just a static class!
constructor(private http: HttpClient) {}
static get(collectionString: string, id: ModelId): BaseModel | undefined {
const collection: Collection = DS[collectionString];
get(collectionString: string, id: ModelId): BaseModel | undefined {
const collection: Collection = this.store[collectionString];
if (!collection) {
return;
}
const model: BaseModel = collection[id];
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) {
return [];
}
return Object.values(collection);
}
// TODO: type for callback function
static filter(collectionString: string, callback): BaseModel[] {
filter(collectionString: string, callback): BaseModel[] {
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) {
throw new ImproperlyConfiguredError('The model must have an id!');
} else if (collectionString === 'invalid-collection-string') {
throw new ImproperlyConfiguredError('Cannot save a BaseModel');
}
if (model.getCollectionString() !== collectionString) {
throw new ImproperlyConfiguredError('The model you try to insert has not the right collection string');
if (typeof this.store[collectionString] === 'undefined') {
this.store[collectionString] = {};
console.log('made new collection: ', collectionString);
}
if (!DS[collectionString]) {
DS[collectionString] = {};
}
DS[collectionString][model.id] = model;
this.store[collectionString][model.id] = model;
console.log('injected ; ', model);
}
static injectMany(collectionString: string, models: BaseModel[]): void {
injectMany(models: BaseModel[]): void {
models.forEach(model => {
DS.inject(collectionString, model);
this.inject(model);
});
}
static eject(collectionString: string, id: ModelId) {
if (DS[collectionString]) {
delete DS[collectionString][id];
eject(collectionString: string, id: ModelId) {
if (this.store[collectionString]) {
delete this.store[collectionString][id];
}
}
static ejectMany(collectionString: string, ids: ModelId[]) {
ejectMany(collectionString: string, ids: ModelId[]) {
ids.forEach(id => {
DS.eject(collectionString, id);
this.eject(collectionString, id);
});
}
// TODO remove the any there and in BaseModel.
static save(model: BaseModel): Observable<any> {
save(model: BaseModel): Observable<BaseModel> {
if (!model.id) {
throw new ImproperlyConfiguredError('The model must have an id!');
}
const collectionString: string = model.getCollectionString();
// make http request to the server
// if this was a success, inject the model into the DS
return of();
//TODO not tested
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.
static delete(model: BaseModel): Observable<any> {
delete(model: BaseModel): Observable<any> {
if (!model.id) {
throw new ImproperlyConfiguredError('The model must have an id!');
}
const collectionString: string = model.getCollectionString();
// make http request to the server
// if this was a success, eject the model from the DS
return of();
//TODO not tested
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,
password: password
};
return this.http.post<User>('/users/login/', user, httpOptions).pipe(
return this.http.post<any>('/users/login/', user, httpOptions).pipe(
tap(val => {
localStorage.setItem('username', val.username);
this.isLoggedIn = true;
//Set the session cookie in local storrage.
//TODO needs validation
}),
catchError(this.handleError())
);

View File

@ -9,6 +9,12 @@ import { tap } from 'rxjs/operators';
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({
selector: 'app-site',
templateUrl: './site.component.html',
@ -22,7 +28,8 @@ export class SiteComponent implements OnInit {
private websocketService: WebsocketService,
private router: Router,
private breakpointObserver: BreakpointObserver,
private translate: TranslateService
private translate: TranslateService,
private dS: DS
) {}
ngOnInit() {
@ -42,6 +49,7 @@ export class SiteComponent implements OnInit {
// subscribe to the socket
socket.subscribe(response => {
console.log('log : ', response); // will contain all the config variables
this.storeResponse(response);
});
// 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 {
console.log('selected langauge: ', lang);
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']
})
export class StartComponent extends BaseComponent implements OnInit {
constructor(titleService: Title) {
private dS: DS;
constructor(titleService: Title, dS: DS) {
super(titleService);
this.dS = dS;
}
ngOnInit() {
@ -22,19 +25,29 @@ export class StartComponent extends BaseComponent implements OnInit {
test() {
// This can be a basic unit test ;)
console.log(User.get(1));
const user1: User = new User(1);
user1.username = 'testuser';
const user2: User = new User(2);
user2.username = 'testuser2';
// console.log(User.get(1));
const user1: User = new User(32, 'testuser');
const user2: User = new User(42, 'testuser 2');
DS.injectMany(User.getCollectionString(), [user1, user2]);
console.log(User.getAll());
console.log(User.filter(user => user.id === 1));
console.log(`User1 | ID ${user1.id}, Name: ${user1.username}`);
console.log(`User2 | ID ${user2.id}, Name: ${user2.username}`);
DS.eject(User.getCollectionString(), user1.id);
console.log(User.getAll());
console.log(User.filter(user => user.id === 1));
console.log(User.filter(user => user.id === 2));
this.dS.inject(user1);
this.dS.inject(user2);
console.log('All users = ', this.dS.getAll('users/user'));
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