diff --git a/backend/sites/package-lock.json b/backend/sites/package-lock.json index 582fa366..bf9c21cc 100644 --- a/backend/sites/package-lock.json +++ b/backend/sites/package-lock.json @@ -19,6 +19,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", @@ -4511,6 +4512,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, "node_modules/class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", @@ -14221,6 +14227,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, "class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", diff --git a/backend/sites/package.json b/backend/sites/package.json index d1b6711a..451ea007 100644 --- a/backend/sites/package.json +++ b/backend/sites/package.json @@ -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", @@ -87,4 +88,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/backend/sites/src/app/dto/recentView.dto.ts b/backend/sites/src/app/dto/recentView.dto.ts new file mode 100644 index 00000000..9a6bf107 --- /dev/null +++ b/backend/sites/src/app/dto/recentView.dto.ts @@ -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; +} \ No newline at end of file diff --git a/backend/sites/src/app/dto/response/fetchSiteResponse.ts b/backend/sites/src/app/dto/response/fetchSiteResponse.ts index 90fb548e..8250dbe1 100644 --- a/backend/sites/src/app/dto/response/fetchSiteResponse.ts +++ b/backend/sites/src/app/dto/response/fetchSiteResponse.ts @@ -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 @@ -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; +} \ No newline at end of file diff --git a/backend/sites/src/app/entities/recentViews.entity.ts b/backend/sites/src/app/entities/recentViews.entity.ts new file mode 100644 index 00000000..c53fa2af --- /dev/null +++ b/backend/sites/src/app/entities/recentViews.entity.ts @@ -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; +} diff --git a/backend/sites/src/app/entities/sites.entity.ts b/backend/sites/src/app/entities/sites.entity.ts index bd8403be..49317d84 100644 --- a/backend/sites/src/app/entities/sites.entity.ts +++ b/backend/sites/src/app/entities/sites.entity.ts @@ -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",], {}) @@ -216,4 +217,7 @@ export class Sites { @OneToOne(() => SiteCrownLandContaminated, siteCrownLandContaminated => siteCrownLandContaminated.sites) siteCrownLandContaminated: SiteCrownLandContaminated; + + @OneToMany(() => RecentViews, (recentViews) => recentViews.site) + recentViewedSites: RecentViews[]; } diff --git a/backend/sites/src/app/mockData/site.mockData.ts b/backend/sites/src/app/mockData/site.mockData.ts index 8d02076b..0d4d024d 100644 --- a/backend/sites/src/app/mockData/site.mockData.ts +++ b/backend/sites/src/app/mockData/site.mockData.ts @@ -1,5 +1,6 @@ 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"; @@ -7,6 +8,7 @@ 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: [] }; @@ -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', @@ -113,5 +116,6 @@ export const sampleSites: Sites[] = [ classCode2: classCd, // Example class code 2 siteRiskCode2: siteRiskCd, sstCode2: sstCode, - siteCrownLandContaminated: siteCrownLandContaminated + siteCrownLandContaminated: siteCrownLandContaminated, + recentViewedSites: recentViewedSites }]; \ No newline at end of file diff --git a/backend/sites/src/app/resolvers/dashboard.resolver.spec.ts b/backend/sites/src/app/resolvers/dashboard.resolver.spec.ts new file mode 100644 index 00000000..4c7e177f --- /dev/null +++ b/backend/sites/src/app/resolvers/dashboard.resolver.spec.ts @@ -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); + service = module.get(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.'); + }); + }); +}); diff --git a/backend/sites/src/app/resolvers/dashboard.resolver.ts b/backend/sites/src/app/resolvers/dashboard.resolver.ts new file mode 100644 index 00000000..8920985c --- /dev/null +++ b/backend/sites/src/app/resolvers/dashboard.resolver.ts @@ -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.' }; + } +} diff --git a/backend/sites/src/app/services/dashboard.service.spec.ts b/backend/sites/src/app/services/dashboard.service.spec.ts new file mode 100644 index 00000000..495f530a --- /dev/null +++ b/backend/sites/src/app/services/dashboard.service.spec.ts @@ -0,0 +1,143 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { DashboardService } from './dashboard.service'; +import { RecentViews } from '../entities/recentViews.entity'; +import { Repository } from 'typeorm'; +import { RecentViewDto } from '../dto/recentView.dto'; +import { plainToClass } from 'class-transformer'; +import { sampleSites } from '../mockData/site.mockData'; + +describe('DashboardService', () => { + let service: DashboardService; + let recentViewsRepository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DashboardService, + { + provide: getRepositoryToken(RecentViews), + useClass: Repository, + }, + ], + }).compile(); + + service = module.get(DashboardService); + recentViewsRepository = module.get>( + getRepositoryToken(RecentViews), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getRecentView', () => { + it('should return an array of recent views for a given user id', async () => { + const userId = '1'; + const expectedRecentViews = [ + { id: 1, userId:'1', siteId: '1', address: '123 Street', city: 'City', generalDescription: 'Description', whenUpdated: new Date() }, + { id: 2, userId:'1', siteId: '2', address: '456 Street', city: 'City', generalDescription: 'Description', whenUpdated: new Date() }, + ]; + + jest + .spyOn(recentViewsRepository, 'find') + .mockResolvedValueOnce(expectedRecentViews as RecentViews[]); + + const recentViews = await service.getRecentViewsByUserId(userId); + + expect(recentViews).toEqual(expectedRecentViews); + }); + + it('should return an empty array if no recent views are found for a given user id', async () => { + const userId = '2'; + const expectedRecentViews: RecentViews[] = []; + + jest + .spyOn(recentViewsRepository, 'find') + .mockResolvedValueOnce(expectedRecentViews); + + const recentViews = await service.getRecentViewsByUserId(userId); + + expect(recentViews).toEqual(expectedRecentViews); + }); + + it('should throw an error if repository throws an error', async () => { + const userId = '1'; + + jest + .spyOn(recentViewsRepository, 'find') + .mockRejectedValueOnce(new Error('Database connection error')); + + await expect(service.getRecentViewsByUserId(userId)).rejects.toThrow('Database connection error'); + }); + }); + + describe('addRecentView', () => { + it('should insert a new recent view if the combination does not exist', async () => { + + // Mock recentViewsRepository methods + jest.spyOn(recentViewsRepository, 'findOne').mockResolvedValue(null); + jest.spyOn(recentViewsRepository, 'count').mockResolvedValue(0); + jest.spyOn(recentViewsRepository, 'save').mockResolvedValueOnce({} as RecentViews); // Mock save method + + // Prepare test data + const recentViewDto = { + userId: '1', + siteId: '1', + address: '123 Street', + city: 'City', + generalDescription: 'Description', + whenUpdated: new Date(), + }; + + // Execute the method + const result = await service.addRecentView(recentViewDto); + + // Assert the result + expect(result).toBe('Record is inserted successfully.'); + }); + + + it('should update an existing recent view if the combination exists', async () => { + const existingRecentView: RecentViews = { + 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] + }; + + const recentViewDto: RecentViewDto = { + userId: '1', + siteId: '1', + address: '456 Street', + city: 'City', + generalDescription: 'New Description', + whenUpdated: new Date(), + }; + + jest + .spyOn(recentViewsRepository, 'findOne') + .mockResolvedValueOnce(existingRecentView); + + const recentViewEntity = plainToClass(RecentViews, recentViewDto); + jest + .spyOn(recentViewsRepository, 'save') + .mockResolvedValueOnce(recentViewEntity); + + const result = await service.addRecentView(recentViewDto); + + expect(result).toBe('Record is updated successfully.'); + }); + }); +}); diff --git a/backend/sites/src/app/services/dashboard.service.ts b/backend/sites/src/app/services/dashboard.service.ts new file mode 100644 index 00000000..f237a848 --- /dev/null +++ b/backend/sites/src/app/services/dashboard.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { RecentViews } from '../entities/recentViews.entity'; +import { Repository } from 'typeorm'; +import { RecentViewDto } from '../dto/recentView.dto'; +import { plainToInstance } from 'class-transformer'; + +@Injectable() +export class DashboardService { + constructor( + @InjectRepository(RecentViews) + private recentViewsRepository: Repository, + ) {} + + async getRecentViewsByUserId(userId: string): Promise { + try { + return await this.recentViewsRepository.find({ where: { userId } }); + } catch (error) { + throw error; + } + } + + async addRecentView(recentViewDto: RecentViewDto) { + const { userId, siteId } = recentViewDto; + const maxRecentViews = 5; // Maximum allowed recent views per user + + try { + // Check if the combination of userId and siteId exists in the table + const existingRecentView = await this.recentViewsRepository.findOne({where: { userId, siteId }}); + + if (existingRecentView) { + // If the combination exists, update the existing record + existingRecentView.address = recentViewDto.address; + existingRecentView.city = recentViewDto.city; + existingRecentView.generalDescription = + recentViewDto.generalDescription; + existingRecentView.whenUpdated = recentViewDto.whenUpdated; + + // Explicitly update the 'updated' column + existingRecentView.updated = new Date(); + const result = await this.recentViewsRepository.save(existingRecentView); + + if (result) { + return 'Record is updated successfully.'; + } + } else { + // If the combination does not exist, insert a new record + const existingRecentViewsCount = await this.recentViewsRepository.count({ where: { userId } }); + + if (existingRecentViewsCount >= maxRecentViews) { + // Delete the oldest recent view if the maximum limit is reached + const oldestRecentView = await this.recentViewsRepository.findOne({ + where: { userId }, + order: { created: 'ASC' }, + }); + await this.recentViewsRepository.delete(oldestRecentView.id); + } + + // Convert the DTO to entity + const newRecentView = plainToInstance(RecentViews, recentViewDto); + const result = await this.recentViewsRepository.save(newRecentView); + + if (result) { + return 'Record is inserted successfully.'; + } + } + } catch (error) { + throw new Error('Failed to insert or update recent view.'); + } + } +} diff --git a/backend/sites/src/app/site.module.ts b/backend/sites/src/app/site.module.ts index 4b3750e3..89ae97a8 100644 --- a/backend/sites/src/app/site.module.ts +++ b/backend/sites/src/app/site.module.ts @@ -44,6 +44,9 @@ import { LtoDownload } from './entities/ltoDownload.entity'; import { LtoPrevDownload } from './entities/ltoPrevDownload.entity'; import { PlanTable } from './entities/planTable.entity'; import { SiteCrownLandContaminated } from './entities/siteCrownLandContaminated.entity'; +import { DashboardResolver } from './resolvers/dashboard.resolver'; +import { DashboardService } from './services/dashboard.service'; +import { RecentViews } from './entities/recentViews.entity'; /** * Module for wrapping all functionalities in sites microserivce @@ -55,10 +58,12 @@ import { SiteCrownLandContaminated } from './entities/siteCrownLandContaminated. ProfileAnswers, ProfileSubmissions, SiteProfileLandUses, SiteProfileOwners, ProfileQuestions, ProfileCategories, SubmissionCd, SiteDocPartics, PeopleOrgs, SiteParticRoles, ParticRoleCd, EventParticRoleCd, CityRegions, SiteContaminationClassXref, ContaminationClassCd, SiteCrownLandStatusCd, SisAddresses, SiteStaffs, DocParticRoleCd, LtoDownload, LtoPrevDownload, - PlanTable, SiteCrownLandContaminated])], + PlanTable, SiteCrownLandContaminated, RecentViews])], providers: [ SiteResolver, SiteService, + DashboardResolver, + DashboardService ], controllers: [SiteController], }) diff --git a/backend/sites/src/migrations/1715628483959-master-script.ts b/backend/sites/src/migrations/1715628483959-master-script.ts new file mode 100644 index 00000000..a747cb3e --- /dev/null +++ b/backend/sites/src/migrations/1715628483959-master-script.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MasterScript1715628483959 implements MigrationInterface { + name = 'MasterScript1715628483959' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sites"."site_crown_land_contaminated" ALTER COLUMN "estimated_cost_of_remediations" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."site_crown_land_contaminated" ALTER COLUMN "actual_cost_of_remediations" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."sites" ALTER COLUMN "latdeg" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."sites" ALTER COLUMN "longdeg" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."sites" ALTER COLUMN "geometry" TYPE geometry`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sites"."sites" ALTER COLUMN "geometry" TYPE geometry(GEOMETRY,0)`); + await queryRunner.query(`ALTER TABLE "sites"."sites" ALTER COLUMN "longdeg" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."sites" ALTER COLUMN "latdeg" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."site_crown_land_contaminated" ALTER COLUMN "actual_cost_of_remediations" TYPE double precision`); + await queryRunner.query(`ALTER TABLE "sites"."site_crown_land_contaminated" ALTER COLUMN "estimated_cost_of_remediations" TYPE double precision`); + } + +}