はじめに
リレーション(一対多)の実装に引き続き、権限付加とQueryBuilderの実装を行いました。
実装したコードを確認したい方は以下よりご確認ください。
実装
管理者のみに特定の操作の権限を与える処理とQueryBuilderで見積額を取得する処理を実装しました。
管理者によるReportの承認
ユーザが作成したReportを管理者(Administrator)が承認する処理を行うルートハンドラー(approveReport)をReportsControllerに追加します。
管理者のみに承認の認可を与えるために、リクエストを行うユーザが管理者であるかどうかを判断するGuardをルートハンドラーの前に置きます。
@Controller('reports')
export class ReportsController {
constructor(private reportsService: ReportsService) {}
...
@Patch('/:id')
@UseGuards(AdminGuard)
approveReport(@Param('id') id: string, @Body() body: ApproveReportDto) {
return this.reportsService.changeApproval(id, body.approved);
}
}
Guard(AdminGuard)の中身は以下のようになります。
currentUser.admin
がtrue
のときだけリクエストを通します。
import { CanActivate, ExecutionContext } from '@nestjs/common';
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
if (!request.currentUser) {
return false;
}
return request.currentUser.admin;
}
}
ただ、AdminGuardを加えただけだとうまく動作しません。
なぜかというと、currentUser
を返すInterceptorはGuardの後に実行されるためです。
そのため、currentUser
を返す処理をInterceptorからMiddlewareにうつしてあげる必要があります。
declare global {
namespace Express {
interface Request {
currentUser?: User;
}
}
}
@Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
constructor(private usersService: UsersService) {}
async use(req: Request, res: Response, next: NextFunction) {
const { userId } = req.session || {};
if (userId) {
const user = await this.usersService.findOne(userId);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
req.currentUser = user;
}
next();
}
}
作成したMiddlewareをUsersModuleに付加します。
export class UsersModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CurrentUserMiddleware).forRoutes('*');
}
}
QueryBuilderで見積額を取得
入力した中古車情報から見積額price
を取得する処理を実装します。
price
を推測するにあたり、以下の条件に当てはまるReportをDBから3件取得します。
そして、3件のレポートのprice
の平均値を見積額として返します。
- make: 同じメーカ
- model: 同じ車種
- lng: +/- 5°
- lat: +/- 5°
- year: 3年以内
- mileage: 近い順
DBからprice
の平均値を取得するにあたって、SQLを構築する必要があります。
そこでTypeORMのQueryBuilderを使用します。
@Injectable()
export class ReportsService {
constructor(@InjectRepository(Report) private repo: Repository<Report>) {}
createEstimate({ make, model, lng, lat, year, mileage }: GetEstimateDto) {
return this.repo
.createQueryBuilder()
.select('AVG(price)', 'price')
.where('make = :make', { make })
.andWhere('model = :model', { model })
.andWhere('lng - :lng BETWEEN -5 AND 5', { lng })
.andWhere('lat - :lat BETWEEN -5 AND 5', { lat })
.andWhere('year - :year BETWEEN -3 AND 3', { year })
.andWhere('approved IS TRUE')
.orderBy('ABS(mileage - :mileage)', 'DESC')
.setParameters({ mileage })
.limit(3)
.getRawOne();
}
...
}
クエリに必要なデータについては、クエリパラメータとしてURLに渡すようにします。
@Controller('reports')
export class ReportsController {
constructor(private reportsService: ReportsService) {}
@Get()
getEstimate(@Query() query: GetEstimateDto) {
return this.reportsService.createEstimate(query);
}
...
}
ただ、クエリパラメータはstring型で入ってくるので、year
やmileage
などについては、GetEstimateDto
内で@Transform()
デコレータを使用して、型を適切に変換してあげる必要があります。
export class GetEstimateDto {
@IsString()
make: string;
@IsString()
model: string;
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(1930)
@Max(2050)
year: number;
@Transform(({ value }) => parseFloat(value))
@IsLongitude()
lng: number;
@Transform(({ value }) => parseFloat(value))
@IsLatitude()
lat: number;
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(0)
@Max(1000000)
mileage: number;
}
参考資料