Skip to content

Commit

Permalink
Merge pull request #699 from bcgov/feat/srs-293-api
Browse files Browse the repository at this point in the history
Dashboard Api files
  • Loading branch information
nikhila-aot authored May 28, 2024
2 parents 632e5fa + 08d37a5 commit 26ee1ec
Show file tree
Hide file tree
Showing 13 changed files with 520 additions and 5 deletions.
11 changes: 11 additions & 0 deletions backend/sites/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/sites/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@nestjs/platform-express": "^10.3.8",
"@nestjs/typeorm": "^10.0.2",
"apollo-server-express": "^3.10.2",
"class-transformer": "^0.5.1",
"graphql": "^16.6.0",
"keycloak-connect": "^24.0.3",
"nest-keycloak-connect": "^1.10.0",
Expand Down Expand Up @@ -87,4 +88,4 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
}
36 changes: 36 additions & 0 deletions backend/sites/src/app/dto/recentView.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsOptional, IsDate, IsInt, Min } from 'class-validator';

@InputType()
export class RecentViewDto {

@Field()
@IsNotEmpty()
@IsString()
userId: string;

@Field()
@IsNotEmpty()
@IsString()
siteId: string;

@Field()
@IsNotEmpty()
@IsString()
address: string;

@Field()
@IsNotEmpty()
@IsString()
city: string;

@Field({nullable:true})
@IsOptional()
@IsString()
generalDescription: string | null;

@Field({nullable:true})
@IsOptional()
@IsDate()
whenUpdated: Date | null;
}
10 changes: 9 additions & 1 deletion backend/sites/src/app/dto/response/fetchSiteResponse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Sites } from '../../entities/sites.entity';

import { BaseHttpResponse } from './baseHttpResponse';
import { RecentViews } from '../../entities/recentViews.entity';

/**
* Class for returing fetch site response from graphql services
Expand Down Expand Up @@ -41,3 +41,11 @@ export class SearchSiteResponse {
}


@ObjectType()
export class DashboardResponse extends BaseHttpResponse{
@Field({nullable:true})
message: string;

@Field(() => [RecentViews], { nullable: true })
data: RecentViews[] | null;
}
63 changes: 63 additions & 0 deletions backend/sites/src/app/entities/recentViews.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Field, ObjectType } from '@nestjs/graphql';
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
JoinColumn,
BeforeUpdate,
} from 'typeorm';
import { Sites } from './sites.entity';
import { SiteStaffs } from './siteStaffs.entity';

@ObjectType()
@Entity('recent_views')
@Index('idx_user_id', ['userId'])
export class RecentViews {
@PrimaryGeneratedColumn()
id: number;

@Field()
@Column('character varying', { name: 'user_id', length: 30 })
userId: string;

@Field()
@Column('character varying', { name: 'site_id' })
siteId: string;

@Field()
@Column('character varying', { length: 200 })
address: string;

@Field()
@Column('character varying', { name: 'city', length: 30 })
city: string;

@Field({ nullable: true })
@Column('character varying', {
name: 'general_description',
length: 225,
nullable: true,
})
generalDescription: string | null;

@Field({ nullable: true })
@Column('timestamp without time zone', {
name: 'when_updated',
nullable: true,
})
whenUpdated: Date | null;

@CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
created: Date;

@UpdateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updated: Date;

@ManyToOne(() => Sites, (site) => site.recentViewedSites)
@JoinColumn({ name: 'site_id', referencedColumnName: 'id' })
site: Sites;
}
4 changes: 4 additions & 0 deletions backend/sites/src/app/entities/sites.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ClassificationCd } from './classificationCd.entity';
import { SiteRiskCd } from './siteRiskCd.entity';
import { SiteStatusCd } from './siteStatusCd.entity';
import { SiteCrownLandContaminated } from './siteCrownLandContaminated.entity'
import { RecentViews } from './recentViews.entity';

@ObjectType()
@Index("site_bco", ["bcerCode", "classCode", "id", "rwmFlag", "sstCode",], {})
Expand Down Expand Up @@ -216,4 +217,7 @@ export class Sites {

@OneToOne(() => SiteCrownLandContaminated, siteCrownLandContaminated => siteCrownLandContaminated.sites)
siteCrownLandContaminated: SiteCrownLandContaminated;

@OneToMany(() => RecentViews, (recentViews) => recentViews.site)
recentViewedSites: RecentViews[];
}
8 changes: 6 additions & 2 deletions backend/sites/src/app/mockData/site.mockData.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { BceRegionCd } from "../entities/bceRegionCd.entity";
import { ClassificationCd } from "../entities/classificationCd.entity";
import { RecentViews } from "../entities/recentViews.entity";
import { SiteCrownLandContaminated } from "../entities/siteCrownLandContaminated.entity";
import { SiteRiskCd } from "../entities/siteRiskCd.entity";
import { SiteStatusCd } from "../entities/siteStatusCd.entity";
import { Sites } from "../entities/sites.entity";


const siteCrownLandContaminated = new SiteCrownLandContaminated();
const recentViewedSites = [new RecentViews()];
const sstCode: SiteStatusCd = { code: '1', description: 'test', sites: [], eventTypeCds: [] };
const siteRiskCd: SiteRiskCd = { code: '1', description: 'test', sites: [] };
const bceRegionCd: BceRegionCd = { code: '1', description: 'test', cityRegions: [], mailouts: [], peopleOrgs: [], sites: [] };
Expand Down Expand Up @@ -62,7 +64,8 @@ export const sampleSites: Sites[] = [
classCode2: classCd, // Example class code 2
siteRiskCode2: siteRiskCd,
sstCode2: sstCode,
siteCrownLandContaminated: siteCrownLandContaminated
siteCrownLandContaminated: siteCrownLandContaminated,
recentViewedSites: recentViewedSites
},
{
id: '222',
Expand Down Expand Up @@ -113,5 +116,6 @@ export const sampleSites: Sites[] = [
classCode2: classCd, // Example class code 2
siteRiskCode2: siteRiskCd,
sstCode2: sstCode,
siteCrownLandContaminated: siteCrownLandContaminated
siteCrownLandContaminated: siteCrownLandContaminated,
recentViewedSites: recentViewedSites
}];
103 changes: 103 additions & 0 deletions backend/sites/src/app/resolvers/dashboard.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DashboardResolver } from './dashboard.resolver';
import { DashboardService } from '../services/dashboard.service';
import { DashboardResponse } from '../dto/response/fetchSiteResponse';
import { sampleSites } from '../mockData/site.mockData';
import { RecentViews } from '../../app/entities/recentViews.entity';
import { RecentViewDto } from '../dto/recentView.dto';

describe('DashboardResolver', () => {
let resolver: DashboardResolver;
let service: DashboardService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DashboardResolver,
{
provide: DashboardService,
useValue: {
getRecentViewsByUserId: jest.fn(),
addRecentView: jest.fn(),
},
},
],
}).compile();

resolver = module.get<DashboardResolver>(DashboardResolver);
service = module.get<DashboardService>(DashboardService);
});


afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(resolver).toBeDefined();
});

describe('getRecentViewsByUserId', () => {
const res = [
{ id: 1, userId:'1', siteId: '1', address: '123 Street', city: 'City', generalDescription: 'Description', whenUpdated: new Date(), created: new Date(), updated: new Date(), site: sampleSites[0] },
{ id: 2, userId:'1', siteId: '2', address: '456 Street', city: 'City', generalDescription: 'Description', whenUpdated: new Date(), created: new Date(), updated: new Date(), site: sampleSites[0] },
];
it('should return recent views for valid userId', async () => {
const userId = '1';
const expectedResult: DashboardResponse = {
httpStatusCode: 200,
message: 'Success',
data: res,
};
jest.spyOn(service, 'getRecentViewsByUserId').mockResolvedValueOnce(expectedResult.data);

const result = await resolver.getRecentViewsByUserId(userId);

expect(result).toEqual(expectedResult);
expect(service.getRecentViewsByUserId).toHaveBeenCalledWith(userId);
});

it('should handle data not found for user id', async () => {
const userId = '1';
jest.spyOn(service, 'getRecentViewsByUserId').mockResolvedValueOnce(null);

const result = await resolver.getRecentViewsByUserId(userId);

expect(result.httpStatusCode).toEqual(404);
expect(result.message).toContain(userId);
expect(result.data).toBeNull();
});
});

describe('addRecentView', () => {
const recentViewDto = {
userId: '1',
siteId: '1',
address: '123 Street',
city: 'City',
generalDescription: 'Description',
whenUpdated: new Date(),
};

it('should add recent view for valid input', async () => {
const recentView: RecentViewDto = recentViewDto;
const expectedResult ='Record is inserted successfully.';
jest.spyOn(service, 'addRecentView').mockResolvedValueOnce(expectedResult);

const result = await resolver.addRecentView(recentView);

expect(result.httpStatusCode).toEqual(201);
expect(result.message).toEqual(expectedResult);
});

it('should handle bad request', async () => {
const recentView: RecentViewDto = recentViewDto;
jest.spyOn(service, 'addRecentView').mockResolvedValueOnce(null);

const result = await resolver.addRecentView(recentView);

expect(result.httpStatusCode).toEqual(400);
expect(result.message).toEqual('Bad Request.');
});
});
});
44 changes: 44 additions & 0 deletions backend/sites/src/app/resolvers/dashboard.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { RecentViews } from '../entities/recentViews.entity';
import { DashboardService } from '../services/dashboard.service';
import { ValidationPipe } from '@nestjs/common';
import { RecentViewDto } from '../dto/recentView.dto';
import { DashboardResponse } from '../dto/response/fetchSiteResponse';
import { RoleMatchingMode, Roles } from 'nest-keycloak-connect';

@Resolver(() => RecentViews)
export class DashboardResolver {
constructor(private readonly dashboardService: DashboardService) {}

@Roles({ roles: ['site-admin'], mode: RoleMatchingMode.ANY })
@Query(() => DashboardResponse, { name: 'getRecentViewsByUserId' })
async getRecentViewsByUserId(
@Args('userId', { type: () => String }) userId: string,
) {
const result = await this.dashboardService.getRecentViewsByUserId(userId);
if (result) {
return { httpStatusCode: 200, message: 'Success', data: result };
}

return {
httpStatusCode: 404,
message: `Data not found for user id: ${userId}`,
data: result,
};
}

@Roles({ roles: ['site-admin'], mode: RoleMatchingMode.ANY })
@Mutation(() => DashboardResponse, { name: 'addRecentView' })
async addRecentView(
@Args('recentView', { type: () => RecentViewDto }, new ValidationPipe())
recentView: RecentViewDto,
) {
const result = await this.dashboardService.addRecentView(recentView);

if (result) {
return { httpStatusCode: 201, message: result };
}

return { httpStatusCode: 400, message: 'Bad Request.' };
}
}
Loading

0 comments on commit 26ee1ec

Please sign in to comment.