diff --git a/src/config/default-filters.config.json b/src/config/default-filters.config.json new file mode 100644 index 000000000..848bb1970 --- /dev/null +++ b/src/config/default-filters.config.json @@ -0,0 +1,11 @@ +[ + { "type": "LocationFilterComponent", "visible": true }, + { "type": "PidFilterComponent", "visible": true }, + { "type": "PidFilterContainsComponent", "visible": false }, + { "type": "PidFilterStartsWithComponent", "visible": false }, + { "type": "GroupFilterComponent", "visible": true }, + { "type": "TypeFilterComponent", "visible": true }, + { "type": "KeywordFilterComponent", "visible": true }, + { "type": "DateRangeFilterComponent", "visible": true }, + { "type": "TextFilterComponent", "visible": true } +] \ No newline at end of file diff --git a/src/users/dto/create-user-settings.dto.ts b/src/users/dto/create-user-settings.dto.ts index 923d616bf..63095f1c2 100644 --- a/src/users/dto/create-user-settings.dto.ts +++ b/src/users/dto/create-user-settings.dto.ts @@ -1,7 +1,9 @@ import { ApiProperty } from "@nestjs/swagger"; import { UpdateUserSettingsDto } from "./update-user-settings.dto"; +import { IsString } from "class-validator"; export class CreateUserSettingsDto extends UpdateUserSettingsDto { @ApiProperty({ type: String, required: true }) + @IsString() readonly userId: string; } diff --git a/src/users/dto/update-user-settings.dto.ts b/src/users/dto/update-user-settings.dto.ts index 9c6741b9e..8458e4e84 100644 --- a/src/users/dto/update-user-settings.dto.ts +++ b/src/users/dto/update-user-settings.dto.ts @@ -1,14 +1,30 @@ import { ApiProperty, PartialType } from "@nestjs/swagger"; +import { + FilterConfig, + ScientificCondition, +} from "../schemas/user-settings.schema"; +import { IsArray, IsNumber } from "class-validator"; export class UpdateUserSettingsDto { @ApiProperty() + @IsArray() readonly columns: Record[]; @ApiProperty({ type: Number, required: false, default: 25 }) + @IsNumber() readonly datasetCount?: number; @ApiProperty({ type: Number, required: false, default: 25 }) + @IsNumber() readonly jobCount?: number; + + @ApiProperty() + @IsArray() + readonly filters: FilterConfig[]; + + @ApiProperty() + @IsArray() + readonly conditions: ScientificCondition[]; } export class PartialUpdateUserSettingsDto extends PartialType( diff --git a/src/users/interceptors/create-user-settings.interceptor.ts b/src/users/interceptors/create-user-settings.interceptor.ts index d059602db..e22d4f881 100644 --- a/src/users/interceptors/create-user-settings.interceptor.ts +++ b/src/users/interceptors/create-user-settings.interceptor.ts @@ -8,6 +8,7 @@ import { import { Observable, tap } from "rxjs"; import { CreateUserSettingsDto } from "../dto/create-user-settings.dto"; import { UsersService } from "../users.service"; +import { FILTER_CONFIGS } from "../schemas/user-settings.schema"; @Injectable() export class CreateUserSettingsInterceptor implements NestInterceptor { @@ -34,6 +35,8 @@ export class CreateUserSettingsInterceptor implements NestInterceptor { const createUserSettingsDto: CreateUserSettingsDto = { userId, columns: [], + filters: FILTER_CONFIGS, + conditions: [], }; return this.usersService.createUserSettings( userId, diff --git a/src/users/interceptors/default-user-settings.interceptor.ts b/src/users/interceptors/default-user-settings.interceptor.ts new file mode 100644 index 000000000..61b8049e9 --- /dev/null +++ b/src/users/interceptors/default-user-settings.interceptor.ts @@ -0,0 +1,33 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from "@nestjs/common"; +import { map, Observable } from "rxjs"; +import { UsersService } from "../users.service"; +import { FILTER_CONFIGS } from "../schemas/user-settings.schema"; +import { UpdateUserSettingsDto } from "../dto/update-user-settings.dto"; + +@Injectable() +export class DefaultUserSettingsInterceptor implements NestInterceptor { + constructor(private usersService: UsersService) {} + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + return next.handle().pipe( + map(async () => { + Logger.log("DefaultUserSettingsInterceptor"); + const defaultUserSettings: UpdateUserSettingsDto = { + columns: [], + filters: FILTER_CONFIGS, + conditions: [], + }; + console.log(defaultUserSettings); + return defaultUserSettings; + }), + ); + } +} diff --git a/src/users/schemas/user-settings.schema.ts b/src/users/schemas/user-settings.schema.ts index d0682b629..9cfe2cae2 100644 --- a/src/users/schemas/user-settings.schema.ts +++ b/src/users/schemas/user-settings.schema.ts @@ -2,9 +2,37 @@ import * as mongoose from "mongoose"; import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { ApiProperty } from "@nestjs/swagger"; import { Document } from "mongoose"; +import filterConfigs from "../../config/default-filters.config.json"; export type UserSettingsDocument = UserSettings & Document; +// Define possible filter component types as a union of string literals +export type FilterComponentType = + | "LocationFilterComponent" + | "PidFilterComponent" + | "PidFilterContainsComponent" + | "PidFilterStartsWithComponent" + | "GroupFilterComponent" + | "TypeFilterComponent" + | "KeywordFilterComponent" + | "DateRangeFilterComponent" + | "TextFilterComponent"; + +// Define the Filter interface +export interface FilterConfig { + type: FilterComponentType; + visible: boolean; +} + +// Define the Condition interface +export interface ScientificCondition { + field: string; + value: string; + operator: string; +} + +export const FILTER_CONFIGS: FilterConfig[] = filterConfigs as FilterConfig[]; + @Schema({ collection: "UserSetting", toJSON: { @@ -43,6 +71,25 @@ export class UserSettings { @ApiProperty({ type: String, required: true }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }) userId: string; + + @ApiProperty({ + type: [Object], + default: FILTER_CONFIGS, + description: "Array of filters the user has set", + }) + @Prop({ + type: [{ type: Object }], + default: FILTER_CONFIGS, + }) + filters: FilterConfig[]; + + @ApiProperty({ + type: [Object], + default: [], + description: "Array of conditions the user has set", + }) + @Prop({ type: [{ type: Object }], default: [] }) + conditions: ScientificCondition[]; } export const UserSettingsSchema = SchemaFactory.createForClass(UserSettings); diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 1b22d0350..6bedb6203 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -3,17 +3,43 @@ import { AuthService } from "src/auth/auth.service"; import { CaslModule } from "src/casl/casl.module"; import { UsersController } from "./users.controller"; import { UsersService } from "./users.service"; +import { UpdateUserSettingsDto } from "./dto/update-user-settings.dto"; class UsersServiceMock { findByIdUserIdentity(id: string) { return { id }; } + + async findByIdUserSettings(userId: string) { + return mockUserSettings; + } + + async findOneAndUpdateUserSettings( + userId: string, + updateUserSettingsDto: UpdateUserSettingsDto, + ) { + return { ...updateUserSettingsDto, _id: userId }; + } } +const mockUserSettings = { + _id: "user1", + userId: "user1", + columns: [], + datasetCount: 25, + jobCount: 25, + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + ], + conditions: [{ field: "status", value: "active", operator: "equals" }], +}; + class AuthServiceMock {} describe("UsersController", () => { let controller: UsersController; + let usersService: UsersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -26,9 +52,58 @@ describe("UsersController", () => { }).compile(); controller = module.get(UsersController); + usersService = module.get(UsersService); + + // bypass authorization + jest + .spyOn(controller as UsersController, "checkUserAuthorization") + .mockImplementation(() => Promise.resolve()); }); it("should be defined", () => { expect(controller).toBeDefined(); }); + + it("should return user settings with filters and conditions", async () => { + jest + .spyOn(usersService, "findByIdUserSettings") + .mockResolvedValue(mockUserSettings); + + const userId = "user1"; + const result = await controller.getSettings( + { user: { _id: userId } }, + userId, + ); + + expect(result).toEqual(mockUserSettings); + expect(result.filters).toBeDefined(); + expect(result.filters.length).toBeGreaterThan(0); + expect(result.conditions).toBeDefined(); + expect(result.conditions.length).toBeGreaterThan(0); + }); + + it("should update user settings with filters and conditions", async () => { + const updatedSettings = { + ...mockUserSettings, + filters: [{ type: "PidFilterContainsComponent", visible: false }], + conditions: [{ field: "status", value: "inactive", operator: "equals" }], + }; + + jest + .spyOn(usersService, "findOneAndUpdateUserSettings") + .mockResolvedValue(updatedSettings); + + const userId = "user-id"; + const result = await controller.updateSettings( + { user: { _id: userId } }, + userId, + updatedSettings, + ); + + expect(result).toEqual(updatedSettings); + expect(result.filters).toBeDefined(); + expect(result.filters.length).toBe(1); + expect(result.conditions).toBeDefined(); + expect(result.conditions.length).toBe(1); + }); }); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index f5438de8b..56e415374 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -12,6 +12,7 @@ import { Body, ForbiddenException, HttpCode, + CanActivate, } from "@nestjs/common"; import { ApiBearerAuth, @@ -31,7 +32,10 @@ import { Request } from "express"; import { JWTUser } from "../auth/interfaces/jwt-user.interface"; import { UserSettings } from "./schemas/user-settings.schema"; import { CreateUserSettingsDto } from "./dto/create-user-settings.dto"; -import { PartialUpdateUserSettingsDto } from "./dto/update-user-settings.dto"; +import { + PartialUpdateUserSettingsDto, + UpdateUserSettingsDto, +} from "./dto/update-user-settings.dto"; import { User } from "./schemas/user.schema"; import { CreateUserSettingsInterceptor } from "./interceptors/create-user-settings.interceptor"; import { AuthService } from "src/auth/auth.service"; @@ -44,6 +48,8 @@ import { CreateCustomJwt } from "./dto/create-custom-jwt.dto"; import { AuthenticatedPoliciesGuard } from "../casl/guards/auth-check.guard"; import { ReturnedUserDto } from "./dto/returned-user.dto"; import { ReturnedAuthLoginDto } from "src/auth/dto/returnedLogin.dto"; +import { PoliciesGuard } from "src/casl/guards/policies.guard"; +import { DefaultUserSettingsInterceptor } from "./interceptors/default-user-settings.interceptor"; @ApiBearerAuth() @ApiTags("users") @@ -307,6 +313,22 @@ export class UsersController { return this.usersService.findOneAndDeleteUserSettings(id); } + @UseInterceptors(DefaultUserSettingsInterceptor) + @UseGuards( + class ByPassAuthenticatedPoliciesGuard + extends PoliciesGuard + implements CanActivate + { + async canActivate(): Promise { + return Promise.resolve(true); + } + }, + ) + @Get("/settings/default") + async getDefaultSettings(): Promise { + return Promise.resolve(new UserSettings()); + } + @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies((ability: AppAbility) => { return ( diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts index 24a65a86d..d69ca6012 100644 --- a/src/users/users.service.spec.ts +++ b/src/users/users.service.spec.ts @@ -29,6 +29,18 @@ const mockUser: User = { datasetCount: 25, jobCount: 25, userId: "testUserId", + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + { type: "TextFilterComponent", visible: true }, + ], + conditions: [], }, }; @@ -58,6 +70,18 @@ const mockUserSettings: UserSettings = { datasetCount: 25, jobCount: 25, userId: "testUserId", + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + { type: "PidFilterContainsComponent", visible: false }, + { type: "PidFilterStartsWithComponent", visible: false }, + { type: "GroupFilterComponent", visible: true }, + { type: "TypeFilterComponent", visible: true }, + { type: "KeywordFilterComponent", visible: true }, + { type: "DateRangeFilterComponent", visible: true }, + { type: "TextFilterComponent", visible: true }, + ], + conditions: [], }; describe("UsersService", () => { diff --git a/test/Users.js b/test/Users.js index 6a51db891..c4f8d7f66 100644 --- a/test/Users.js +++ b/test/Users.js @@ -47,7 +47,7 @@ describe("2360: Users settings", () => { accessTokenUser1 = loginResponseUser1.token; }); - it("0010: Update users settings with valid value should sucess ", async () => { + it("0010: Update users settings with valid value should success ", async () => { return request(appUrl) .put(`/api/v3/Users/${userIdUser1}/settings`) .set("Accept", "application/json") @@ -58,6 +58,8 @@ describe("2360: Users settings", () => { res.body.should.have.property("userId", userIdUser1); res.body.should.have.property("datasetCount"); res.body.should.have.property("jobCount"); + res.body.should.have.property("filters"); + res.body.should.have.property("conditions"); }); }); });