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> = 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.
This commit is contained in:
Jerko Steiner 2019-08-06 19:26:08 +07:00
parent a057ca97d5
commit b617069784
3 changed files with 203 additions and 0 deletions

View File

@ -16,3 +16,6 @@ export * from './filterProps'
export * from './indexBy'
export * from './types'
export * from './without'
import * as team from './team'
export {team}

View File

@ -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<T> = (context: IContext) => Promise<T>
export interface ITeamService {
create(params: ITeamCreateParams): Contextual<ITeam>
remove(params: ITeamRemoveParams): Contextual<{id: number}>
update(params: ITeamUpdateParams): Contextual<ITeam>
addUser(params: ITeamAddUserParams): Contextual<IUserInTeam>
removeUser(params: ITeamAddUserParams): Contextual<ITeamAddUserParams>
findOne(id: number): Promise<ITeam | undefined>
find(userId: number): Contextual<ITeam[]>
findUsers(teamId: number): Contextual<IUserInTeam[]>
// TODO add other methods
}

View File

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