From b6170697843b894c27e8a8ed4ef739492d07e35f Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Tue, 6 Aug 2019 19:26:08 +0700 Subject: [PATCH] Add packages/services/src/services/TeamService2.ts A few notes: 1) The context higher-order function will be tedious to define - a lot more typing than just an extra function argument. 2) As some methods do not require the context function, forgetting to call it might introduce bugs (await fn will not error out), but compile checks of the return value type might detect this Possible solution is to go the GRPC way and allow only a single method type as a parameter so all server-side method types would look like: async fn(param: IParam, context: Context) {} and all client-side method types would look like: async fn(param: IParam) {} However, This would deviate from the JSON-RPC standard which allows multiple arguments to be passed in an array. Alternatively, context could be passed as a first argument and then filtered out: type Arguments = T extends (context: Ctx, ...args: infer A) => infer R ? A : never In this case, the type of Ctx would need to be known in advance. Will have to think about this some more. --- packages/common/src/index.ts | 3 + packages/common/src/team.ts | 47 ++++++ packages/server/src/services/TeamService2.ts | 153 +++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 packages/common/src/team.ts create mode 100644 packages/server/src/services/TeamService2.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 70b71ae..f42456a 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -16,3 +16,6 @@ export * from './filterProps' export * from './indexBy' export * from './types' export * from './without' + +import * as team from './team' +export {team} diff --git a/packages/common/src/team.ts b/packages/common/src/team.ts new file mode 100644 index 0000000..ee14d41 --- /dev/null +++ b/packages/common/src/team.ts @@ -0,0 +1,47 @@ +import {ITeam} from './ITeam' +import {IUserInTeam} from './IUserInTeam' + +export interface ITeamAddUserParams { + teamId: number + userId: number + roleId: number +} + +export interface ITeamCreateParams { + name: string +} + +export interface ITeamRemoveParams { + id: number +} + +export interface ITeamUpdateParams { + id: number + name: string +} + +export interface IContext { + userId: number +} + +type Contextual = (context: IContext) => Promise + +export interface ITeamService { + create(params: ITeamCreateParams): Contextual + + remove(params: ITeamRemoveParams): Contextual<{id: number}> + + update(params: ITeamUpdateParams): Contextual + + addUser(params: ITeamAddUserParams): Contextual + + removeUser(params: ITeamAddUserParams): Contextual + + findOne(id: number): Promise + + find(userId: number): Contextual + + findUsers(teamId: number): Contextual + + // TODO add other methods +} diff --git a/packages/server/src/services/TeamService2.ts b/packages/server/src/services/TeamService2.ts new file mode 100644 index 0000000..d76d183 --- /dev/null +++ b/packages/server/src/services/TeamService2.ts @@ -0,0 +1,153 @@ +import {DB} from '../database/DB' +import {Validator} from '../validator' +import {Team} from '../entities/Team' +import {UserTeam} from '../entities/UserTeam' +import {IUserPermissions} from '../user/IUserPermissions' +import { + trim, + team as t, + IUserInTeam, +} from '@rondo/common' + +type IContext = t.IContext + +export class TeamService2 implements t.ITeamService { + constructor( + protected readonly db: DB, + protected readonly permissions: IUserPermissions, + ) {} + + create = (params: t.ITeamCreateParams) => async (context: IContext) => { + const {userId} = context + const name = trim(params.name) + + new Validator({name, userId}) + .ensure('name') + .ensure('userId') + .throw() + + const team = await this.db.getRepository(Team).save({ + name, + userId, + }) + + await this.addUser({ + teamId: team.id, + userId, + // ADMIN role + roleId: 1, + })(context) + + return team + } + + remove = ({id}: t.ITeamRemoveParams) => async (context: IContext) => { + const {userId} = context + + await this.permissions.belongsToTeam({ + teamId: id, + userId: context.userId, + }) + + await this.db.getRepository(UserTeam) + .delete({teamId: id, userId}) + + await this.db.getRepository(Team) + .delete(id) + + return {id} + } + + update = ({id, name}: t.ITeamUpdateParams) => async (context: IContext) => { + await this.permissions.belongsToTeam({ + teamId: id, + userId: context.userId, + }) + + await this.db.getRepository(Team) + .update({ + id, + }, { + name, + }) + + return (await this.findOne(id))! + } + + addUser = (params: t.ITeamAddUserParams) => async (context: IContext) => { + const {userId, teamId, roleId} = params + await this.db.getRepository(UserTeam) + .save({userId, teamId, roleId}) + + const userTeam = await this.createFindUserInTeamQuery() + .where({ + userId, + teamId, + roleId, + }) + .getOne() + + return this.mapUserInTeam(userTeam!) + } + + removeUser = (params: t.ITeamAddUserParams) => async (context: IContext) => { + const {teamId, userId, roleId} = params + + await this.permissions.belongsToTeam({ + teamId: params.teamId, + userId: context.userId, + }) + + // TODO check if this is the last admin team member + await this.db.getRepository(UserTeam) + .delete({userId, teamId, roleId}) + + return {teamId, userId, roleId} + } + + async findOne(id: number) { + return this.db.getRepository(Team).findOne(id) + } + + find = () => async ({userId}: IContext) => { + return this.db.getRepository(Team) + .createQueryBuilder('team') + .select('team') + .innerJoin('team.userTeams', 'ut') + .where('ut.userId = :userId', {userId}) + .getMany() + } + + findUsers = (teamId: number) => async (context: IContext) => { + await this.permissions.belongsToTeam({ + teamId, + userId: context.userId, + }) + + const userTeams = await this.createFindUserInTeamQuery() + .where('ut.teamId = :teamId', { + teamId, + }) + .getMany() + + return userTeams.map(this.mapUserInTeam) + } + + protected mapUserInTeam(ut: UserTeam): IUserInTeam { + return { + teamId: ut.teamId, + userId: ut.userId, + displayName: `${ut.user.firstName} ${ut.user.lastName}`, + roleId: ut.role!.id, + roleName: ut.role!.name, + } + } + + protected createFindUserInTeamQuery() { + return this.db.getRepository(UserTeam) + .createQueryBuilder('ut') + .select('ut') + .innerJoinAndSelect('ut.user', 'user') + .innerJoinAndSelect('ut.role', 'role') + } +}