src/proposals/proposals.controller.ts
proposals
Methods |
|
| Async create | |||||||||
create(request: Request, createProposalDto: CreateProposalDto)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:126
|
|||||||||
|
Parameters :
Returns :
Promise<ProposalClass>
|
| Async createAttachment | |||||||||
createAttachment(proposalId: string, createAttachmentDto: CreateAttachmentDto)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:477
|
|||||||||
|
Parameters :
Returns :
Promise<Attachment>
|
| Async findAll | |||||||||
findAll(request: Request, filters?: string)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:205
|
|||||||||
|
Parameters :
Returns :
Promise<ProposalClass[]>
|
| Async findAllAttachments | ||||||
findAllAttachments(proposalId: string)
|
||||||
Decorators :
@UseGuards(PoliciesGuard)
|
||||||
|
Defined in src/proposals/proposals.controller.ts:510
|
||||||
|
Parameters :
Returns :
Promise<Attachment[]>
|
| Async findAllDatasets | ||||||
findAllDatasets(proposalId: string)
|
||||||
Decorators :
@UseGuards(PoliciesGuard)
|
||||||
|
Defined in src/proposals/proposals.controller.ts:620
|
||||||
|
Parameters :
Returns :
Promise<[] | null>
|
| Async findById | |||||||||
findById(request: Request, proposalId: string)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:350
|
|||||||||
|
Parameters :
Returns :
Promise<ProposalClass | null>
|
| Async findOneAttachmentAndRemove |
findOneAttachmentAndRemove(proposalId: string, attachmentId: string)
|
Decorators :
@UseGuards(PoliciesGuard)
|
|
Defined in src/proposals/proposals.controller.ts:585
|
|
Returns :
Promise<>
|
| Async findOneAttachmentAndUpdate | ||||||||||||
findOneAttachmentAndUpdate(proposalId: string, attachmentId: string, updateAttachmentDto: UpdateAttachmentDto)
|
||||||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
||||||||||||
|
Defined in src/proposals/proposals.controller.ts:546
|
||||||||||||
|
Parameters :
Returns :
Promise<Attachment | null>
|
| Async fullfacet | |||||||||
fullfacet(request: Request, filters: literal type)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:303
|
|||||||||
|
Parameters :
Returns :
Promise<Record[]>
|
| Async fullquery | |||||||||
fullquery(request: Request, filters: literal type)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:250
|
|||||||||
|
Parameters :
Returns :
Promise<ProposalClass[]>
|
| Async isValid | ||||
isValid(createProposal)
|
||||
Decorators :
@AllowAny()
|
||||
|
Defined in src/proposals/proposals.controller.ts:161
|
||||
|
Parameters :
Returns :
Promise<literal type>
|
| Async remove | ||||||
remove(proposalId: string)
|
||||||
Decorators :
@UseGuards()
|
||||||
|
Defined in src/proposals/proposals.controller.ts:446
|
||||||
|
Parameters :
Returns :
Promise<>
|
| Async update | |||||||||
update(proposalId: string, updateProposalDto: UpdateProposalDto)
|
|||||||||
Decorators :
@UseGuards(PoliciesGuard)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:417
|
|||||||||
|
Parameters :
Returns :
Promise<ProposalClass | null>
|
| updateFiltersForList | |||||||||
updateFiltersForList(request: Request, mergedFilters: IFilters<ProposalDocument | IProposalFields>)
|
|||||||||
|
Defined in src/proposals/proposals.controller.ts:75
|
|||||||||
|
Parameters :
|
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
Query,
UseInterceptors,
HttpCode,
HttpStatus,
Req,
ForbiddenException,
ConflictException,
} from "@nestjs/common";
import { Request } from "express";
import { ProposalsService } from "./proposals.service";
import { CreateProposalDto } from "./dto/create-proposal.dto";
import { UpdateProposalDto } from "./dto/update-proposal.dto";
import {
ApiBearerAuth,
ApiBody,
ApiExtraModels,
ApiOperation,
ApiParam,
ApiQuery,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { PoliciesGuard } from "src/casl/guards/policies.guard";
import { CheckPolicies } from "src/casl/decorators/check-policies.decorator";
import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory";
import { Action } from "src/casl/action.enum";
import { ProposalClass, ProposalDocument } from "./schemas/proposal.schema";
import { AttachmentsService } from "src/attachments/attachments.service";
import { Attachment } from "src/attachments/schemas/attachment.schema";
import { CreateAttachmentDto } from "src/attachments/dto/create-attachment.dto";
import { UpdateAttachmentDto } from "src/attachments/dto/update-attachment.dto";
import { DatasetsService } from "src/datasets/datasets.service";
import { DatasetClass } from "src/datasets/schemas/dataset.schema";
import { IProposalFields } from "./interfaces/proposal-filters.interface";
import { MultiUTCTimeInterceptor } from "src/common/interceptors/multi-utc-time.interceptor";
import { MeasurementPeriodClass } from "./schemas/measurement-period.schema";
import {
IFacets,
IFilters,
ILimitsFilter,
} from "src/common/interfaces/common.interface";
import { AllowAny } from "src/auth/decorators/allow-any.decorator";
import { plainToInstance } from "class-transformer";
import { validate, ValidatorOptions } from "class-validator";
import {
filterDescription,
filterExample,
fullQueryDescriptionLimits,
fullQueryExampleLimits,
proposalsFullQueryDescriptionFields,
proposalsFullQueryExampleFields,
} from "src/common/utils";
import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
@ApiBearerAuth()
@ApiTags("proposals")
@Controller("proposals")
export class ProposalsController {
constructor(
private readonly attachmentsService: AttachmentsService,
private readonly datasetsService: DatasetsService,
private readonly proposalsService: ProposalsService,
private caslAbilityFactory: CaslAbilityFactory,
) {}
updateFiltersForList(
request: Request,
mergedFilters: IFilters<ProposalDocument, IProposalFields>,
): IFilters<ProposalDocument, IProposalFields> {
const user: JWTUser = request.user as JWTUser;
if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, ProposalClass);
const canViewTheirOwn = ability.can(Action.ListOwn, ProposalClass);
if (!canViewAll && canViewTheirOwn) {
if (!mergedFilters.where) {
mergedFilters.where = {};
}
mergedFilters.where["$or"] = [
{ ownerGroup: { $in: user.currentGroups } },
{ accessGroups: { $in: user.currentGroups } },
];
}
}
return mergedFilters;
}
// POST /proposals
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Create, ProposalClass),
)
@UseInterceptors(
new MultiUTCTimeInterceptor<ProposalClass, MeasurementPeriodClass>(
"MeasurementPeriodList",
["start", "end"],
),
)
@Post()
@ApiOperation({
summary: "It creates a new proposal.",
description:
"It creates a new proposal and returnes it completed with systems fields.",
})
@ApiExtraModels(CreateProposalDto)
@ApiBody({
type: CreateProposalDto,
})
@ApiResponse({
status: 201,
type: ProposalClass,
description:
"Create a new proposal and return its representation in SciCat",
})
async create(
@Req() request: Request,
@Body() createProposalDto: CreateProposalDto,
): Promise<ProposalClass> {
const existingProposal = await this.findById(
request,
createProposalDto.proposalId,
);
if (existingProposal) {
throw new ConflictException(
`Proposal with ${createProposalDto.proposalId} already exists`,
);
}
return this.proposalsService.create(createProposalDto);
}
@AllowAny()
@HttpCode(HttpStatus.OK)
@Post("/isValid")
@ApiOperation({
summary: "It validates the proposal provided as input.",
description:
"It validates the proposal provided as input, and returns true if the information is a valid proposal",
})
@ApiBody({
type: CreateProposalDto,
})
@ApiResponse({
status: 200,
type: Boolean,
description:
"Check if the proposal provided pass validation. It return true if the validation is passed",
})
async isValid(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Body() createProposal: unknown,
): Promise<{ valid: boolean }> {
const validatorOptions: ValidatorOptions = {
skipMissingProperties: true,
whitelist: true,
forbidNonWhitelisted: true,
};
const dtoProposal = plainToInstance(CreateProposalDto, createProposal);
const errorsProposal = await validate(dtoProposal, validatorOptions);
const valid = errorsProposal.length == 0;
return { valid: valid };
}
// GET /proposals
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Read, ProposalClass),
)
@Get()
@ApiOperation({
summary: "It returns a list of proposals.",
description:
"It returns a list of proposals. The list returned can be modified by providing a filter.",
})
@ApiQuery({
name: "filters",
description:
"Database filters to apply when retrieving proposals\n" +
filterDescription,
required: false,
type: String,
example: filterExample,
})
@ApiResponse({
status: 200,
type: ProposalClass,
isArray: true,
description: "Return the proposals requested",
})
async findAll(
@Req() request: Request,
@Query("filters") filters?: string,
): Promise<ProposalClass[]> {
const proposalFilters: IFilters<ProposalDocument, IProposalFields> =
this.updateFiltersForList(request, JSON.parse(filters ?? "{}"));
return this.proposalsService.findAll(proposalFilters);
}
// GET /proposals/fullquery
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Read, ProposalClass),
)
@Get("/fullquery")
@ApiOperation({
summary: "It returns a list of proposals matching the query provided.",
description:
"It returns a list of proposals matching the query provided.<br>This endpoint still needs some work on the query specification.",
})
@ApiQuery({
name: "fields",
description:
"Full query filters to apply when retrieving proposals\n" +
proposalsFullQueryDescriptionFields,
required: false,
type: String,
example: proposalsFullQueryExampleFields,
})
@ApiQuery({
name: "limits",
description:
"Define further query parameters like skip, limit, order\n" +
fullQueryDescriptionLimits,
required: false,
type: String,
example: fullQueryExampleLimits,
})
@ApiResponse({
status: 200,
type: ProposalClass,
isArray: true,
description: "Return proposals requested",
})
async fullquery(
@Req() request: Request,
@Query() filters: { fields?: string; limits?: string },
): Promise<ProposalClass[]> {
const user: JWTUser = request.user as JWTUser;
const fields: IProposalFields = JSON.parse(filters.fields ?? "{}");
const limits: ILimitsFilter = JSON.parse(filters.limits ?? "{}");
if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, ProposalClass);
// NOTE: If we have published true we don't add groups at all
if (!canViewAll) {
fields.userGroups = fields.userGroups ?? [];
fields.userGroups.push(...user.currentGroups);
}
}
const parsedFilters: IFilters<ProposalDocument, IProposalFields> = {
fields,
limits,
};
return this.proposalsService.fullquery(parsedFilters);
}
// GET /proposals/fullfacet
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Read, ProposalClass),
)
@Get("/fullfacet")
@ApiOperation({
summary:
"It returns a list of proposal facets matching the filter provided.",
description:
"It returns a list of proposal facets matching the filter provided.<br>This endpoint still needs some work on the filter and facets specification.",
})
@ApiQuery({
name: "filters",
description:
"Full facet query filters to apply when retrieving proposals\n" +
proposalsFullQueryDescriptionFields,
required: false,
type: String,
example: proposalsFullQueryExampleFields,
})
@ApiResponse({
status: 200,
type: ProposalClass,
isArray: true,
description: "Return proposals requested",
})
async fullfacet(
@Req() request: Request,
@Query() filters: { fields?: string; facets?: string },
): Promise<Record<string, unknown>[]> {
const user: JWTUser = request.user as JWTUser;
const fields: IProposalFields = JSON.parse(filters.fields ?? "{}");
const facets = JSON.parse(filters.facets ?? "[]");
if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, ProposalClass);
// NOTE: If we have published true we don't add groups at all
if (!canViewAll) {
fields.userGroups = fields.userGroups ?? [];
fields.userGroups.push(...user.currentGroups);
}
}
const parsedFilters: IFacets<IProposalFields> = {
fields,
facets,
};
return this.proposalsService.fullfacet(parsedFilters);
}
// GET /proposals/:id
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Read, ProposalClass),
)
@Get("/:pid")
@ApiOperation({
summary: "It returns the proposal requested.",
description: "It returns the proposal requested through the pid specified.",
})
@ApiParam({
name: "pid",
description: "Id of the proposal to return",
type: String,
})
@ApiResponse({
status: 200,
type: ProposalClass,
isArray: false,
description: "Return proposal with pid specified",
})
async findById(
@Req() request: Request,
@Param("pid") proposalId: string,
): Promise<ProposalClass | null> {
const proposal = await this.proposalsService.findOne({
proposalId: proposalId,
});
const user: JWTUser = request.user as JWTUser;
if (proposal) {
// NOTE: We need ProposalClass instance because casl module can not recognize the type from proposal mongo database model. If other fields are needed can be added later.
const proposalInstance = new ProposalClass();
proposalInstance._id = proposal._id;
proposalInstance.accessGroups = proposal.accessGroups;
proposalInstance.ownerGroup = proposal.ownerGroup;
proposalInstance.proposalId = proposal.proposalId;
proposalInstance.email = proposal.email;
if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canView =
ability.can(Action.Manage, proposalInstance) ||
ability.can(Action.Read, proposalInstance);
if (!canView) {
throw new ForbiddenException("Unauthorized access");
}
} else {
throw new ForbiddenException("Unauthorized access");
}
}
return proposal;
}
// PATCH /proposals/:pid
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Update, ProposalClass),
)
@UseInterceptors(
new MultiUTCTimeInterceptor<ProposalClass, MeasurementPeriodClass>(
"MeasurementPeriodList",
["start", "end"],
),
)
@Patch("/:pid")
@ApiOperation({
summary: "It updates the proposal.",
description:
"It updates the proposal specified through the pid specified. it updates only the specified fields.",
})
@ApiParam({
name: "pid",
description: "Id of the proposal to modify",
type: String,
})
@ApiExtraModels(UpdateProposalDto)
@ApiBody({
type: UpdateProposalDto,
})
@ApiResponse({
status: 200,
type: ProposalClass,
description:
"Update an existing proposal and return its representation in SciCat",
})
async update(
@Param("pid") proposalId: string,
@Body() updateProposalDto: UpdateProposalDto,
): Promise<ProposalClass | null> {
return this.proposalsService.update(
{ proposalId: proposalId },
updateProposalDto,
);
}
// DELETE /proposals/:id
@UseGuards()
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Delete, ProposalClass),
)
@Delete("/:pid")
@ApiOperation({
summary: "It deletes the proposal.",
description: "It delete the proposal specified through the pid specified.",
})
@ApiParam({
name: "pid",
description: "Id of the proposal to delete",
type: String,
})
@ApiResponse({
status: 200,
description: "No value is returned",
})
async remove(@Param("pid") proposalId: string): Promise<unknown> {
return this.proposalsService.remove({ proposalId: proposalId });
}
// POST /proposals/:id/attachments
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Create, Attachment),
)
@Post("/:pid/attachments")
@ApiOperation({
summary: "It creates a new attachement for the proposal specified.",
description:
"It creates a new attachement for the proposal specified by the pid passed.",
})
@ApiParam({
name: "pid",
description:
"Id of the proposal we would like to create a new attachment for",
type: String,
})
@ApiExtraModels(CreateAttachmentDto)
@ApiBody({
type: CreateAttachmentDto,
})
@ApiResponse({
status: 201,
type: Attachment,
description:
"Create a new attachment for the proposal identified by the pid specified",
})
async createAttachment(
@Param("pid") proposalId: string,
@Body() createAttachmentDto: CreateAttachmentDto,
): Promise<Attachment> {
const createAttachment = {
...createAttachmentDto,
proposalId: proposalId,
};
return this.attachmentsService.create(createAttachment);
}
// GET /proposals/:pid/attachments
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Attachment))
@Get("/:pid/attachments")
@ApiOperation({
summary: "It returns all the attachments for the proposal specified.",
description:
"It returns all the attachments for the proposal specified by the pid passed.",
})
@ApiParam({
name: "pid",
description:
"Id of the proposal for which we would like to retrieve all the attachments",
type: String,
})
@ApiResponse({
status: 200,
type: Attachment,
isArray: true,
description:
"Array with all the attachments associated with the proposal with the pid specified",
})
async findAllAttachments(
@Param("pid") proposalId: string,
): Promise<Attachment[]> {
return this.attachmentsService.findAll({ proposalId: proposalId });
}
// PATCH /proposals/:pid/attachments/:aid
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Update, Attachment),
)
@Patch("/:pid/attachments/:aid")
@ApiOperation({
summary: "It updates the attachment specified for the proposal indicated.",
description:
"It updates the attachment specified by the aid parameter for the proposal indicated by the pid parameter.<br>This endpoint is obsolete and it will removed in future version.<br>Attachements can be updated from the attachment endpoint.",
})
@ApiParam({
name: "pid",
description:
"Id of the proposal for which we would like to update the attachment specified",
type: String,
})
@ApiParam({
name: "aid",
description:
"Id of the attachment of this proposal that we would like to patch",
type: String,
})
@ApiResponse({
status: 200,
type: Attachment,
isArray: false,
description:
"Update values of the attachment with id specified associated with the proposal with the pid specified",
})
async findOneAttachmentAndUpdate(
@Param("pid") proposalId: string,
@Param("aid") attachmentId: string,
@Body() updateAttachmentDto: UpdateAttachmentDto,
): Promise<Attachment | null> {
return this.attachmentsService.findOneAndUpdate(
{ _id: attachmentId, proposalId: proposalId },
updateAttachmentDto,
);
}
// DELETE /proposals/:pid/attachments/:aid
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Delete, Attachment),
)
@Delete("/:pid/attachments/:aid")
@ApiOperation({
summary: "It deletes the attachment from the proposal.",
description:
"It deletes the attachment from the proposal.<br>This endpoint is obsolete and will be dropped in future versions.<br>Deleting attachments will be done only from the attachements endpoint.",
})
@ApiParam({
name: "pid",
description:
"Id of the proposal for which we would like to delete the attachment specified",
type: String,
})
@ApiParam({
name: "aid",
description:
"Id of the attachment of this proposal that we would like to delete",
type: String,
})
@ApiResponse({
status: 200,
description:
"Remove the attachment with id specified associated with the proposal with the pid specified",
})
async findOneAttachmentAndRemove(
@Param("pid") proposalId: string,
@Param("aid") attachmentId: string,
): Promise<unknown> {
return this.attachmentsService.findOneAndRemove({
_id: attachmentId,
proposalId: proposalId,
});
}
// GET /proposals/:id/datasets
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) =>
ability.can(Action.Read, DatasetClass),
)
@Get("/:pid/datasets")
@ApiOperation({
summary:
"It returns all the datasets associated with the proposal indicated.",
description:
"It returns all the datasets associated with the proposal indicated by the pid parameter.<br>Changes to the related datasets must be performed through the dataset endpoint.",
})
@ApiParam({
name: "pid",
description:
"Id of the proposal for which we would like to retrieve all the datasets",
type: String,
})
@ApiResponse({
status: 200,
type: DatasetClass,
isArray: true,
description:
"Array with all the datasets associated with the proposal with the pid specified",
})
async findAllDatasets(
@Param("pid") proposalId: string,
): Promise<DatasetClass[] | null> {
return this.datasetsService.findAll({ where: { proposalId } });
}
}