Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General Assignments Service Refactor #386

Merged
merged 15 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions services/apps/assignments/src/assignee/assignee.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class AssigneeController {
@Param('assignment', ObjectIdPipe) assignment: Types.ObjectId,
@Param('field') field: string,
): Promise<unknown[]> {
return this.assigneeService.model.distinct(field, {assignment}).exec();
return this.assigneeService.distinct(assignment, field);
}

@Patch('assignments/:assignment/assignees')
Expand All @@ -44,7 +44,7 @@ export class AssigneeController {
@Param('assignment', ObjectIdPipe) assignment: Types.ObjectId,
@Body() dtos: BulkUpdateAssigneeDto[],
): Promise<Assignee[]> {
return Promise.all(dtos.map(dto => this.assigneeService.upsert({assignment, solution: dto.solution}, dto)));
return this.assigneeService.upsertMany(assignment, dtos);
}

@Get('assignments/:assignment/solutions/:solution/assignee')
Expand Down
3 changes: 1 addition & 2 deletions services/apps/assignments/src/assignee/assignee.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {Injectable} from '@nestjs/common';
import {OnEvent} from '@nestjs/event-emitter';
import {SolutionDocument} from '../solution/solution.schema';
import {AssigneeService} from './assignee.service';
import {Types} from "mongoose";

@Injectable()
export class AssigneeHandler {
Expand All @@ -14,7 +13,7 @@ export class AssigneeHandler {
@OnEvent('assignments.*.solutions.*.deleted')
async onSolutionDeleted(solution: SolutionDocument) {
await this.assigneeService.deleteOne({
assignment: new Types.ObjectId(solution.assignment),
assignment: solution.assignment,
solution: solution._id,
});
}
Expand Down
14 changes: 12 additions & 2 deletions services/apps/assignments/src/assignee/assignee.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {EventService, MongooseRepository} from '@mean-stream/nestx';
import {EventRepository, EventService, MongooseRepository} from '@mean-stream/nestx';
import {Injectable} from '@nestjs/common';
import {InjectModel} from '@nestjs/mongoose';
import {Model} from 'mongoose';
import {Model, Types} from 'mongoose';

import {Assignee, AssigneeDocument} from './assignee.schema';
import {BulkUpdateAssigneeDto} from "./assignee.dto";

@Injectable()
@EventRepository()
export class AssigneeService extends MongooseRepository<Assignee, never, AssigneeDocument> {
constructor(
@InjectModel(Assignee.name) public model: Model<Assignee, object, object, object, AssigneeDocument>,
Expand All @@ -17,4 +19,12 @@ export class AssigneeService extends MongooseRepository<Assignee, never, Assigne
private emit(event: string, assignee: AssigneeDocument) {
this.eventService.emit(`assignments.${assignee.assignment}.solutions.${assignee.solution}.assignee.${event}`, assignee);
}

async distinct(assignment: Types.ObjectId, field: string): Promise<unknown[]> {
return this.model.distinct(field, {assignment}).exec();
}

async upsertMany(assignment: Types.ObjectId, dtos: BulkUpdateAssigneeDto[]): Promise<AssigneeDocument[]> {
return Promise.all(dtos.map(dto => this.upsert({assignment, solution: dto.solution}, dto)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Request} from 'express';
import {Observable} from 'rxjs';
import {AssignmentService} from './assignment.service';
import {notFound} from '@mean-stream/nestx';
import {Types} from "mongoose";

@Injectable()
export class AssignmentAuthGuard implements CanActivate {
Expand All @@ -21,7 +22,7 @@ export class AssignmentAuthGuard implements CanActivate {
}

async checkAuth(id: string, user?: UserToken, token?: string): Promise<boolean> {
const assignment = await this.assignmentService.findOne(id) ?? notFound(id);
const assignment = await this.assignmentService.find(new Types.ObjectId(id)) ?? notFound(id);
return this.assignmentService.isAuthorized(assignment, user, token);
}
}
39 changes: 29 additions & 10 deletions services/apps/assignments/src/assignment/assignment.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Auth, AuthUser, UserToken} from '@app/keycloak-auth';
import {NotFound, notFound, ObjectIdArrayPipe} from '@mean-stream/nestx';
import {NotFound, notFound, ObjectIdArrayPipe, ObjectIdPipe} from '@mean-stream/nestx';
import {
Body,
Controller,
Expand All @@ -14,12 +14,13 @@ import {
Query,
} from '@nestjs/common';
import {ApiCreatedResponse, ApiHeader, ApiOkResponse, ApiTags, getSchemaPath} from '@nestjs/swagger';
import {FilterQuery, Types} from 'mongoose';
import {FilterQuery, Types, UpdateQuery} from 'mongoose';
import {AssignmentAuth} from './assignment-auth.decorator';
import {CreateAssignmentDto, ReadAssignmentDto, UpdateAssignmentDto,} from './assignment.dto';
import {Assignment} from './assignment.schema';
import {Assignment, ASSIGNMENT_COLLATION, ASSIGNMENT_SORT} from './assignment.schema';
import {AssignmentService} from './assignment.service';
import {MemberService} from "@app/member";
import {generateToken} from "../utils";

const forbiddenResponse = 'Not owner or invalid Assignment-Token.';

Expand All @@ -39,7 +40,11 @@ export class AssignmentController {
@Body() dto: CreateAssignmentDto,
@AuthUser() user?: UserToken,
) {
return this.assignmentService.create(dto, user?.sub);
return this.assignmentService.create({
...dto,
token: generateToken(),
createdBy: user?.sub,
});
}

@Get()
Expand All @@ -64,7 +69,10 @@ export class AssignmentController {
const members = await this.memberService.findAll({user: {$in: memberIds}});
(filter.$or ||= []).push({_id: {$in: members.map(m => m.parent)}});
}
return (await this.assignmentService.findAll(filter)).map(a => this.assignmentService.mask(a.toObject()));
return (await this.assignmentService.findAll(filter, {
sort: ASSIGNMENT_SORT,
collation: ASSIGNMENT_COLLATION,
})).map(a => this.assignmentService.mask(a.toObject()));
}

@Get(':id')
Expand All @@ -81,11 +89,11 @@ export class AssignmentController {
})
@ApiHeader({name: 'assignment-token', required: false})
async findOne(
@Param('id') id: string,
@Param('id', ObjectIdPipe) id: Types.ObjectId,
@Headers('assignment-token') token?: string,
@AuthUser() user?: UserToken,
): Promise<Assignment | ReadAssignmentDto> {
const assignment = await this.assignmentService.findOne(id) ?? notFound(id);
const assignment = await this.assignmentService.find(id) ?? notFound(id);
if (await this.assignmentService.isAuthorized(assignment, user, token)) {
return assignment;
}
Expand All @@ -97,9 +105,20 @@ export class AssignmentController {
@AssignmentAuth({forbiddenResponse})
@ApiOkResponse({type: Assignment})
async update(
@Param('id') id: string,
@Param('id', ObjectIdPipe) id: Types.ObjectId,
@Body() dto: UpdateAssignmentDto,
): Promise<Assignment | null> {
const {token, classroom, ...rest} = dto;
const update: UpdateQuery<Assignment> = rest;
if (token) {
update.token = generateToken();
}
if (classroom) {
// need to flatten the classroom object to prevent deleting the GitHub token all the time
for (const [key, value] of Object.entries(classroom)) {
update[`classroom.${key}`] = value;
}
}
return this.assignmentService.update(id, dto);
}

Expand All @@ -108,8 +127,8 @@ export class AssignmentController {
@AssignmentAuth({forbiddenResponse})
@ApiOkResponse({type: Assignment})
async remove(
@Param('id') id: string,
@Param('id', ObjectIdPipe) id: Types.ObjectId,
): Promise<Assignment | null> {
return this.assignmentService.remove(id);
return this.assignmentService.delete(id);
}
}
60 changes: 8 additions & 52 deletions services/apps/assignments/src/assignment/assignment.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import {EventService} from '@mean-stream/nestx';
import {EventRepository, EventService, MongooseRepository} from '@mean-stream/nestx';
import {UserToken} from '@app/keycloak-auth';
import {Injectable} from '@nestjs/common';
import {InjectModel} from '@nestjs/mongoose';
import {FilterQuery, Model, Types, UpdateQuery} from 'mongoose';
import {generateToken} from '../utils';
import {CreateAssignmentDto, ReadAssignmentDto, ReadTaskDto, UpdateAssignmentDto} from './assignment.dto';
import {Assignment, ASSIGNMENT_COLLATION, ASSIGNMENT_SORT, AssignmentDocument, Task} from './assignment.schema';
import {Model} from 'mongoose';
import {ReadAssignmentDto, ReadTaskDto} from './assignment.dto';
import {Assignment, AssignmentDocument, Task} from './assignment.schema';
import {MemberService} from "@app/member";

@Injectable()
export class AssignmentService {
@EventRepository()
export class AssignmentService extends MongooseRepository<Assignment> {
constructor(
@InjectModel(Assignment.name) private model: Model<Assignment>,
@InjectModel(Assignment.name) model: Model<Assignment>,
private eventService: EventService,
private readonly memberService: MemberService,
) {
super(model);
}

findTask(tasks: Task[], id: string): Task | undefined {
Expand All @@ -30,28 +31,6 @@ export class AssignmentService {
return undefined;
}

async create(dto: CreateAssignmentDto, userId?: string): Promise<AssignmentDocument> {
const token = generateToken();
const created = await this.model.create({
...dto,
token,
createdBy: userId,
});
created && this.emit('created', created);
return created;
}

async findAll(where: FilterQuery<Assignment> = {}): Promise<AssignmentDocument[]> {
return this.model.find(where, undefined,{
sort: ASSIGNMENT_SORT,
collation: ASSIGNMENT_COLLATION,
}).exec();
}

async findOne(id: string | Types.ObjectId): Promise<AssignmentDocument | null> {
return this.model.findById(id).exec();
}

mask(assignment: Assignment): ReadAssignmentDto {
const {token, tasks, classroom, ...rest} = assignment;
return {
Expand All @@ -68,29 +47,6 @@ export class AssignmentService {
};
}

async update(id: string | Types.ObjectId, dto: UpdateAssignmentDto | UpdateQuery<Assignment>): Promise<Assignment | null> {
const {token, classroom, ...rest} = dto;
const update: UpdateQuery<Assignment> = rest;
if (token) {
update.token = generateToken();
}
if (classroom) {
// need to flatten the classroom object to prevent deleting the GitHub token all the time
for (const [key, value] of Object.entries(classroom)) {
update[`classroom.${key}`] = value;
}
}
const updated = await this.model.findByIdAndUpdate(id, update, {new: true}).exec();
updated && this.emit('updated', updated);
return updated;
}

async remove(id: string): Promise<AssignmentDocument | null> {
const deleted = await this.model.findByIdAndDelete(id).exec();
deleted && this.emit('deleted', deleted);
return deleted;
}

async isAuthorized(assignment: Assignment, user?: UserToken, token?: string): Promise<boolean> {
if (assignment.token === token) {
return true;
Expand Down
11 changes: 6 additions & 5 deletions services/apps/assignments/src/classroom/classroom.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {AssignmentAuth} from '../assignment/assignment-auth.decorator';
import {ClassroomService} from './classroom.service';
import {ImportSolution} from "./classroom.dto";
import {AssignmentService} from "../assignment/assignment.service";
import {notFound} from "@mean-stream/nestx";
import {notFound, ObjectIdPipe} from "@mean-stream/nestx";
import {Types} from "mongoose";

const forbiddenResponse = 'Not owner of assignment, or invalid Assignment-Token.';

Expand All @@ -23,9 +24,9 @@ export class ClassroomController {
@AssignmentAuth({forbiddenResponse})
@ApiOkResponse({type: [ImportSolution]})
async previewImport(
@Param('assignment') id: string,
@Param('assignment', ObjectIdPipe) id: Types.ObjectId,
): Promise<ImportSolution[]> {
const assignment = await this.assignmentService.findOne(id) || notFound(id);
const assignment = await this.assignmentService.find(id) || notFound(id);
return this.classroomService.previewImports(assignment);
}

Expand All @@ -35,11 +36,11 @@ export class ClassroomController {
@AssignmentAuth({forbiddenResponse})
@ApiCreatedResponse({type: [ImportSolution]})
async importSolutions(
@Param('assignment') id: string,
@Param('assignment', ObjectIdPipe) id: Types.ObjectId,
@Query('usernames', new ParseArrayPipe({optional: true})) usernames?: string[],
@UploadedFiles() files?: Express.Multer.File[],
): Promise<ImportSolution[]> {
const assignment = await this.assignmentService.findOne(id) || notFound(id);
const assignment = await this.assignmentService.find(id) || notFound(id);
return files ? this.classroomService.importFiles(assignment, files) : this.classroomService.importSolutions(assignment, usernames);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export class ClassroomService {

private createImportSolution(assignment: AssignmentDocument, repo: RepositoryInfo, commit: string | undefined): ImportSolution {
return {
assignment: assignment._id.toString(),
assignment: assignment._id,
author: {
name: '',
email: '',
Expand Down
3 changes: 2 additions & 1 deletion services/apps/assignments/src/comment/comment-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {CanActivate, ExecutionContext, Injectable} from '@nestjs/common';
import {Request} from 'express';
import {notFound} from '@mean-stream/nestx';
import {CommentService} from './comment.service';
import {Types} from "mongoose";

@Injectable()
export class CommentAuthGuard implements CanActivate {
Expand All @@ -14,7 +15,7 @@ export class CommentAuthGuard implements CanActivate {
const req = context.switchToHttp().getRequest() as Request;
const commentId = req.params.comment ?? req.params.id;
const user = (req as any).user;
const comment = await this.commentService.findOne(commentId) ?? notFound(commentId);
const comment = await this.commentService.find(new Types.ObjectId(commentId)) ?? notFound(commentId);
return this.commentService.isAuthorized(comment, user);
}
}
Loading