Skip to content

Commit

Permalink
Merge pull request #1503 from SciCatProject/SWAP-4299-proposal-type
Browse files Browse the repository at this point in the history
feat: add type property to the proposal document with database migration
  • Loading branch information
martin-trajanovski authored Nov 18, 2024
2 parents 15ee6aa + 8752f52 commit 5cacae3
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ ES_PASSWORD="duo-password"
ES_REFRESH=<"wait_for"|"false">

LOGGERS_CONFIG_FILE="loggers.json"
PROPOSAL_TYPES_FILE="proposalTypes.json"
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,6 @@ jobs:
run: |
cp CI/ESS/docker-compose.api.yaml docker-compose.yaml
cp functionalAccounts.json.test functionalAccounts.json
cp proposalTypes.example.json proposalTypes.json
docker compose up --build -d
npm run test:api
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/dist
/node_modules
functionalAccounts.json
proposalTypes.json
loggers.json

# Configs
Expand Down
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,22 @@ Thank you for your interest in contributing to our project!
3. Add _.env_ file to project root folder. See [Environment variables](#environment-variables).
4. _Optional_ Add [functionalAccounts.json](#local-user-accounts) file to project root folder to create local users.
5. _Optional_ Add [loggers.json](#loggers-configuration) file to the root folder and configure multiple loggers.
6. `npm run start:dev`
7. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.
8. To be able to run the e2e tests with the same setup as in the Github actions you will need to run `npm run prepare:local` and after that run `npm run start:dev`. This will start all needed containers and copy some configuration to the right place.
6. _Optional_ Add [proposalTypes.json](#prpopsal-types-configuration) file to the root folder and configure the proposal types.
7. `npm run start:dev`
8. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.
9. To be able to run the e2e tests with the same setup as in the Github actions you will need to run `npm run prepare:local` and after that run `npm run start:dev`. This will start all needed containers and copy some configuration to the right place.

## Develop in a container using the docker-compose.dev file

1. `git clone https://github.com/SciCatProject/scicat-backend-next.git`
2. docker-compose -f docker-compose.dev.yaml up -d
3. _Optional_ Mount [functionalAccounts.json](#local-user-accounts) file to a volume in the container to create local users.
4. _Optional_ Mount [loggers.json](#loggers-configuration) file to a volume in the container to configure multiple loggers.
5. _Optional_ change the container env variables
6. Attach to the container
7. `npm run start:dev`
8. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.
5. _Optional_ Mount [proposalTypes.json](#prpopsal-types-configuration) file to a volume in the container to configure the proposal types.
6. _Optional_ change the container env variables
7. Attach to the container
8. `npm run start:dev`
9. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.

## Test the app

Expand Down Expand Up @@ -96,6 +98,12 @@ Providing a file called _loggers.json_ at the root of the project, locally or in

The `loggers.json.example` file in the root directory showcases the example of configuration structure for the one or multiple loggers. `logger.service.ts` file contains the configuration handling process logic, and `src/loggers/loggingProviders/grayLogger.ts` includes actual usecase of grayLogger.

### Prpopsal types configuration

Providing a file called _proposalTypes.json_ at the root of the project, locally or in the container, will be automatically loaded into the application configuration service under property called `proposalTypes` and used for validation against proposal creation and update.

The `proposalTypes.json.example` file in the root directory showcases the example of configuration structure for proposal types.

## Environment variables

Valid environment variables for the .env file. See [.env.example](/.env.example) for examples value formats.
Expand Down Expand Up @@ -176,6 +184,7 @@ Valid environment variables for the .env file. See [.env.example](/.env.example)
| `ES_FIELDS_LIMIT` | number | | The total number of fields in an index. | 1000 |
| `ES_REFRESH` | string | | If set to `wait_for`, Elasticsearch will wait till data is inserted into the specified index before returning a response. | false |
| `LOGGERS_CONFIG_FILE` | string | | The file name for loggers configuration, located in the project root directory. | "loggers.json" |
| `PROPOSAL_TYPES_FILE` | string | | The file name for proposal types configuration, located in the project root directory. | "proposalTypes.json" |
| `SWAGGER_PATH` | string | Yes | swaggerPath is the path where the swagger UI will be available| "explorer"|
| `MAX_FILE_UPLOAD_SIZE` | string | Yes | Maximum allowed file upload size | "16mb"|

Expand Down
12 changes: 12 additions & 0 deletions migrations/20241113130700-proposal-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
async up(db, client) {
db.collection("Proposal").updateMany(
{ type: { $exists: false } },
{ $set: { type: "Default Proposal" } },
);
},

async down(db, client) {
db.collection("Proposal").updateMany({}, { $unset: { type: 1 } });
},
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"test:api": "npm run test:api:jest --maxWorkers=50% && concurrently -k -s first \"wait-on http://localhost:3000/explorer/ && npm run test:api:mocha\" \"npm run start\"",
"test:api:jest": "jest --config ./test/config/jest-e2e.json --maxWorkers=50%",
"test:api:mocha": "mocha --config ./test/config/.mocharc.json -r chai/register-should.js",
"prepare:local": "docker-compose -f CI/E2E/docker-compose-local.yaml --env-file CI/E2E/.env.elastic-search up -d && cp functionalAccounts.json.test functionalAccounts.json"
"prepare:local": "docker-compose -f CI/E2E/docker-compose-local.yaml --env-file CI/E2E/.env.elastic-search up -d && cp functionalAccounts.json.test functionalAccounts.json && cp proposalTypes.example.json proposalTypes.json"
},
"dependencies": {
"@casl/ability": "^6.3.2",
Expand Down
4 changes: 4 additions & 0 deletions proposalTypes.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"DOORProposal": "DOOR Proposal",
"Beamtime": "Beamtime"
}
12 changes: 11 additions & 1 deletion src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as fs from "fs";
import { merge } from "lodash";
import localconfiguration from "./localconfiguration";
import { boolean } from "mathjs";
import { DEFAULT_PROPOSAL_TYPE } from "src/proposals/schemas/proposal.schema";

const configuration = () => {
const accessGroupsStaticValues =
Expand Down Expand Up @@ -38,9 +39,12 @@ const configuration = () => {
modulePath: "./loggingProviders/defaultLogger",
config: {},
};
const jsonConfigMap: { [key: string]: object[] | boolean } = {};
const jsonConfigMap: { [key: string]: object | object[] | boolean } = {
proposalTypes: {},
};
const jsonConfigFileList: { [key: string]: string } = {
loggers: process.env.LOGGERS_CONFIG_FILE || "loggers.json",
proposalTypes: process.env.PROPOSAL_TYPES_FILE || "proposalTypes.json",
};
Object.keys(jsonConfigFileList).forEach((key) => {
const filePath = jsonConfigFileList[key];
Expand All @@ -57,6 +61,11 @@ const configuration = () => {
}
});

// NOTE: Add the default proposal type here
Object.assign(jsonConfigMap.proposalTypes, {
DefaultProposal: DEFAULT_PROPOSAL_TYPE,
});

const config = {
maxFileUploadSizeInMb: process.env.MAX_FILE_UPLOAD_SIZE || "16mb", // 16MB by default
versions: {
Expand Down Expand Up @@ -203,6 +212,7 @@ const configuration = () => {
policyPublicationShiftInYears: process.env.POLICY_PUBLICATION_SHIFT ?? 3,
policyRetentionShiftInYears: process.env.POLICY_RETENTION_SHIFT ?? -1,
},
proposalTypes: jsonConfigMap.proposalTypes,
};
return merge(config, localconfiguration);
};
Expand Down
11 changes: 11 additions & 0 deletions src/proposals/dto/update-proposal.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IsArray,
IsDateString,
IsEmail,
IsEnum,
IsObject,
IsOptional,
IsString,
Expand Down Expand Up @@ -133,6 +134,16 @@ export class UpdateProposalDto extends OwnableDto {
@IsOptional()
@IsString()
readonly parentProposalId?: string;

@ApiProperty({
type: String,
required: false,
description:
"Characterize type of proposal, use some of the configured values",
})
@IsOptional()
@IsString()
readonly type?: string;
}

export class PartialUpdateProposalDto extends PartialType(UpdateProposalDto) {}
17 changes: 15 additions & 2 deletions src/proposals/proposals.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { forwardRef, Module } from "@nestjs/common";
import { BadRequestException, forwardRef, Module } from "@nestjs/common";
import { ProposalsService } from "./proposals.service";
import { ProposalsController } from "./proposals.controller";
import { MongooseModule } from "@nestjs/mongoose";
import { ProposalClass, ProposalSchema } from "./schemas/proposal.schema";
import { CaslAbilityFactory } from "src/casl/casl-ability.factory";
import { AttachmentsModule } from "src/attachments/attachments.module";
import { DatasetsModule } from "src/datasets/datasets.module";
import { ConfigModule, ConfigService } from "@nestjs/config";

@Module({
imports: [
Expand All @@ -14,7 +15,11 @@ import { DatasetsModule } from "src/datasets/datasets.module";
MongooseModule.forFeatureAsync([
{
name: ProposalClass.name,
useFactory: () => {
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const proposalTypes = configService.get("proposalTypes") || "{}";
const proposalTypesArray: string[] = Object.values(proposalTypes);
const schema = ProposalSchema;

schema.pre<ProposalClass>("save", function (next) {
Expand All @@ -23,8 +28,16 @@ import { DatasetsModule } from "src/datasets/datasets.module";
if (!this._id) {
this._id = this.proposalId;
}

if (this.type && !proposalTypesArray.includes(this.type)) {
throw new BadRequestException(
`type must be one of the following values: ${proposalTypesArray.join(", ")}`,
);
}

next();
});

return schema;
},
},
Expand Down
22 changes: 12 additions & 10 deletions src/proposals/proposals.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Inject, Injectable, Scope } from "@nestjs/common";
import { Inject, Injectable, NotFoundException, Scope } from "@nestjs/common";
import { REQUEST } from "@nestjs/core";
import { Request } from "express";
import { InjectModel } from "@nestjs/mongoose";
Expand Down Expand Up @@ -97,15 +97,17 @@ export class ProposalsService {
): Promise<ProposalClass | null> {
const username = (this.request.user as JWTUser).username;

return this.proposalModel
.findOneAndUpdate(
filter,
addUpdatedByField(updateProposalDto, username),
{
new: true,
},
)
.exec();
const proposal = await this.proposalModel.findOne(filter);

if (!proposal) {
throw new NotFoundException(`Proposal with filter: ${filter} not found`);
}

Object.assign(proposal, addUpdatedByField(updateProposalDto, username));

const updatedProposal = new this.proposalModel(proposal);

return updatedProposal.save();
}

async remove(filter: FilterQuery<ProposalDocument>): Promise<unknown> {
Expand Down
17 changes: 17 additions & 0 deletions src/proposals/schemas/proposal.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
MeasurementPeriodSchema,
} from "./measurement-period.schema";

// NOTE: This is the proposal default type and it will be used if no proposalTypes.json config file is provided
export const DEFAULT_PROPOSAL_TYPE = "Default Proposal";

export type ProposalDocument = ProposalClass & Document;
@Schema({
collection: "Proposal",
Expand Down Expand Up @@ -182,6 +185,20 @@ export class ProposalClass extends OwnableClass {
ref: "Proposal",
})
parentProposalId: string;

@ApiProperty({
type: String,
required: true,
default: DEFAULT_PROPOSAL_TYPE,
description:
"Characterize type of proposal, use some of the configured values",
})
@Prop({
type: String,
required: true,
default: DEFAULT_PROPOSAL_TYPE,
})
type: string;
}

export const ProposalSchema = SchemaFactory.createForClass(ProposalClass);
Expand Down
41 changes: 41 additions & 0 deletions test/Proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ describe("1500: Proposal: Simple Proposal", () => {
.then((res) => {
res.body.should.have.property("createdBy").and.be.string;
res.body.should.have.property("updatedBy").and.be.string;
res.body.should.have.property("type").and.be.string;
res.body.type.should.be.equal("Default Proposal");
});
});

Expand Down Expand Up @@ -241,6 +243,45 @@ describe("1500: Proposal: Simple Proposal", () => {
});
});

it("0116: adds a new proposal with a type different than default", async () => {
const proposalType = "DOOR Proposal";
const proposalWithType = {
...TestData.ProposalCorrectComplete,
proposalId: faker.string.numeric(8),
type: proposalType,
};

return request(appUrl)
.post("/api/v3/Proposals")
.send(proposalWithType)
.set("Accept", "application/json")
.set({ Authorization: `Bearer ${accessTokenProposalIngestor}` })
.expect(TestData.EntryCreatedStatusCode)
.expect("Content-Type", /json/)
.then((res) => {
res.body.should.have.property("ownerGroup").and.be.string;
res.body.should.have.property("proposalId").and.be.string;
res.body.type.should.be.equal(proposalType);
});
});

it("0117: cannot add a new proposal with a type different than predefined proposal types", async () => {
const proposalType = "Incorrect type";
const proposalWithIncorrectType = {
...TestData.ProposalCorrectComplete,
proposalId: faker.string.numeric(8),
type: proposalType,
};

return request(appUrl)
.post("/api/v3/Proposals")
.send(proposalWithIncorrectType)
.set("Accept", "application/json")
.set({ Authorization: `Bearer ${accessTokenProposalIngestor}` })
.expect(TestData.BadRequestStatusCode)
.expect("Content-Type", /json/);
});

it("0120: updates a proposal with a new parent proposal", async () => {
return request(appUrl)
.patch("/api/v3/Proposals/" + proposalWithParentId)
Expand Down

0 comments on commit 5cacae3

Please sign in to comment.