Skip to content

Commit

Permalink
adjust frontend; move api conversion to own module
Browse files Browse the repository at this point in the history
  • Loading branch information
lukavdplas committed Aug 15, 2023
1 parent be03c07 commit de1d88e
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 33 deletions.
6 changes: 5 additions & 1 deletion frontend/src/app/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import * as _ from 'lodash';

interface UserProfileResponse {
enable_search_history: boolean;
}

export interface UserResponse {
id: number;
username: string;
email: string;
download_limit: number;
is_admin: boolean;
saml: boolean;
enable_search_history: boolean;
profile: UserProfileResponse;
}

export class User {
Expand Down
36 changes: 5 additions & 31 deletions frontend/src/app/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { User, UserResponse } from '../models';
import { ApiService } from './api.service';
import { SessionService } from './session.service';
import * as _ from 'lodash';
import { encodeUserData, parseUserData } from '../utils/user';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -79,7 +80,7 @@ export class AuthService implements OnDestroy {
.getUser()
.pipe(takeUntil(this.destroy$))
.subscribe(
(result) => this.setAuth(this.parseUserResponse(result)),
(result) => this.setAuth(parseUserData(result)),
() => this.purgeAuth()
);
}
Expand All @@ -104,7 +105,7 @@ export class AuthService implements OnDestroy {
const loginRequest$ = this.apiService.login(username, password);
return loginRequest$.pipe(
mergeMap(() => this.checkUser()),
tap((res) => this.setAuth(this.parseUserResponse(res))),
tap((res) => this.setAuth(parseUserData(res))),
catchError((error) => {
console.error(error);
return throwError(error);
Expand Down Expand Up @@ -169,40 +170,13 @@ export class AuthService implements OnDestroy {
}

public updateSettings(update: Partial<User>) {
return this.apiService.updateUserSettings(this.encodeUserUpdate(update)).pipe(
tap((res) => this.setAuth(this.parseUserResponse(res))),
return this.apiService.updateUserSettings(encodeUserData(update)).pipe(
tap((res) => this.setAuth(parseUserData(res))),
catchError((error) => {
console.error(error);
return throwError(error);
})
);
}

/**
* Transforms backend user response to User object
*
* @param result User response data
* @returns User object
*/
private parseUserResponse(
result: UserResponse
): User {
return new User(
result.id,
result.username,
result.is_admin,
result.download_limit == null ? 0 : result.download_limit,
result.saml,
result.enable_search_history,
);
}

private encodeUserUpdate(update: Partial<User>): Partial<UserResponse> {
const changeKeys = {
enableSearchHistory: 'enable_search_history'
};
const transformKey = (value, key, obj) => changeKeys[key] || key;
return _.mapKeys(update, transformKey);
}

}
79 changes: 79 additions & 0 deletions frontend/src/app/utils/user.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as _ from 'lodash';
import { User, UserResponse } from '../models';
import { parseUserData, encodeUserData } from './user';

/**
* check if an object is a partial version of another object
*
* Verify that each key in `part` has the same value in `whole`,
* but ignore any properties of `whole` that are ommitted in `part`.
*/
const isPartialOf = <T>(part: Partial<T>, whole: T): boolean => {
const picked = _.pick(whole, _.keys(part));
return _.isEqual(part, picked);
};

const customMatchers = {
/** expect an object to be a partial version of another object */
toBePartialOf: (matchersUtil) => ({
compare: <T>(actual: Partial<T>, expected: T) => {
const pass = isPartialOf(actual, expected);
return { pass };
}
})
};

describe('user API conversion', () => {
let user: User;
let userResponse: UserResponse;

beforeEach(() => {
jasmine.addMatchers(customMatchers);
});

beforeEach(() => {
user = new User(
1,
'Hamlet',
false,
10000,
false,
true,
);
});

beforeEach(() => {
userResponse = {
id: 1,
username: 'Hamlet',
email: '[email protected]',
download_limit: 10000,
is_admin: false,
saml: false,
profile: {
enable_search_history: true,
}
};
});

it('should convert a user response to a user object', () => {
expect(parseUserData(userResponse)).toEqual(user);
});

it('should convert a user to a user response object', () => {
const encoded = encodeUserData(user);
(expect(encoded) as any).toBePartialOf(userResponse);
});

it('should define inverse functions', () => {
const encoded = encodeUserData(user);
const decoded = parseUserData(encoded as UserResponse);
expect(decoded).toEqual(user);

const parsed = parseUserData(userResponse);
const unparsed = encodeUserData(parsed);
// this has to be a partial match because User contains a subset of the information
// in the API
(expect(unparsed) as any).toBePartialOf(userResponse);
});
});
44 changes: 44 additions & 0 deletions frontend/src/app/utils/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as _ from 'lodash';
import { User, UserResponse } from '../models';

/* Transforms backend user response to User object
*
* @param result User response data
* @returns User object
*/
export const parseUserData = (result: UserResponse): User => new User(
result.id,
result.username,
result.is_admin,
result.download_limit == null ? 0 : result.download_limit,
result.saml,
result.profile.enable_search_history,
);

/**
* Transfroms User data to backend UserResponse object
*
* Because this is used for patching, the data can be partial
*
* @param data (partial) User object
* @returns UserResponse object
*/
export const encodeUserData = (data: Partial<User>): Partial<UserResponse> => {
const changeKeys = {
name: 'username',
isAdmin: 'is_admin',
downloadLimit: 'download_limit',
isSamlLogin: 'saml',
enableSearchHistory: 'profile.enable_search_history'
};

const encoded = {};

_.keys(data).forEach(key => {
const value = data[key];
const path = changeKeys[key] ? _.toPath(changeKeys[key]) : key;
_.set(encoded, path, value);
});

return encoded;
};
4 changes: 3 additions & 1 deletion frontend/src/mock-data/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ export const mockUserResponse: UserResponse = {
email: '[email protected]',
download_limit: 10000,
saml: false,
enable_search_history: true,
profile: {
enable_search_history: true,
},
};

0 comments on commit de1d88e

Please sign in to comment.