diff --git a/api/src/models/project-create.test.ts b/api/src/models/project-create.test.ts index 6de87eb0..03faba56 100644 --- a/api/src/models/project-create.test.ts +++ b/api/src/models/project-create.test.ts @@ -6,6 +6,7 @@ import { PostFundingSource, PostIUCNData, PostLocationData, + PostObjectivesData, PostPartnershipsData, PostProjectData, PostProjectObject @@ -42,6 +43,10 @@ describe('PostProjectObject', () => { it('sets partnerships', function () { expect(projectPostObject.partnership).to.eql([]); }); + + it('sets objectives', function () { + expect(projectPostObject.objective).to.eql([]); + }); }); describe.skip('All values provided', () => { @@ -123,7 +128,8 @@ describe('PostProjectObject', () => { } ] }, - partnerships: ['partner1, partner2'] + partnerships: ['partner1, partner2'], + objectives: ['objective1, obective2'] }; before(() => { @@ -289,6 +295,34 @@ describe('PostPartnershipsData', () => { }); }); +describe('PostObjectivesData', () => { + describe('No values provided', () => { + let projectObjectiveData: PostObjectivesData; + + before(() => { + projectObjectiveData = new PostObjectivesData(null); + }); + + it('sets projectObjectivesData', function () { + expect(projectObjectiveData.objectives).to.eql([]); + }); + }); + + describe.skip('All values provided', () => { + let projectObjectiveData: PostObjectivesData; + + const obj = ['1', '2']; + + before(() => { + projectObjectiveData = new PostObjectivesData(obj); + }); + + it('sets projectObjectivesData', function () { + expect(projectObjectiveData.objectives).to.eql(obj); + }); + }); +}); + describe('PostFundingSource', () => { describe('No values provided', () => { let projectFundingData: PostFundingSource; diff --git a/api/src/models/project-create.ts b/api/src/models/project-create.ts index a791858d..c5747cf1 100644 --- a/api/src/models/project-create.ts +++ b/api/src/models/project-create.ts @@ -18,6 +18,7 @@ export class PostProjectObject { iucn: PostIUCNData; funding: PostFundingData; partnership: PostPartnershipsData; + objective: PostObjectivesData; focus: PostFocusData; restoration_plan: PostRestPlanData; @@ -32,6 +33,7 @@ export class PostProjectObject { this.funding = (obj?.funding && new PostFundingData(obj.funding)) || null; this.iucn = (obj?.iucn && new PostIUCNData(obj.iucn)) || null; this.partnership = (obj?.partnership && new PostPartnershipsData(obj.partnership)) || []; + this.objective = (obj?.objective && new PostObjectivesData(obj.objective)) || []; this.focus = (obj?.focus && new PostFocusData(obj.focus)) || []; this.restoration_plan = (obj?.restoration_plan && new PostRestPlanData(obj.restoration_plan)) || null; } @@ -158,6 +160,33 @@ export class PostPartnershipsData { } } +export interface IPostObjective { + objective: string; +} + +/** + * Processes POST /project objectives data + * + * @export + * @class PostObjectivesData + */ +export class PostObjectivesData { + objectives: IPostObjective[]; + + constructor(obj?: any) { + defaultLog.debug({ label: 'PostObjectivesData', message: 'params', obj }); + + this.objectives = + (obj?.objectives?.length && + obj.objectives.map((item: any) => { + return { + objective: item.objective + }; + })) || + []; + } +} + /** * Processes POST /project project data. * diff --git a/api/src/models/project-update.test.ts b/api/src/models/project-update.test.ts index 22f26f99..2a4da74a 100644 --- a/api/src/models/project-update.test.ts +++ b/api/src/models/project-update.test.ts @@ -1,6 +1,13 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { PutFundingData, PutIUCNData, PutLocationData, PutPartnershipsData, PutProjectData } from './project-update'; +import { + PutFundingData, + PutIUCNData, + PutLocationData, + PutObjectivesData, + PutPartnershipsData, + PutProjectData +} from './project-update'; describe('PutLocationData', () => { describe('No values provided', () => { @@ -153,6 +160,36 @@ describe('PutPartnershipsData', () => { }); }); +describe('PutObjectivesData', () => { + describe('No values provided', () => { + let data: PutObjectivesData; + + before(() => { + data = new PutObjectivesData(null); + }); + + it('sets objectives', () => { + expect(data.objectives).to.eql([]); + }); + }); + + describe('all values provided', () => { + const obj = { + objectives: ['objective 3', 'objective 4'] + }; + + let data: PutObjectivesData; + + before(() => { + data = new PutObjectivesData(obj); + }); + + it('sets objectives', () => { + expect(data.objectives).to.eql(obj.objectives); + }); + }); +}); + describe('PutProjectData', () => { describe('No values provided', () => { let data: PutProjectData; diff --git a/api/src/models/project-update.ts b/api/src/models/project-update.ts index 5ece3ba5..3a88961a 100644 --- a/api/src/models/project-update.ts +++ b/api/src/models/project-update.ts @@ -63,6 +63,14 @@ export class PutPartnershipsData { } } +export class PutObjectivesData { + objectives: string[]; + + constructor(obj?: any) { + this.objectives = (obj?.objectives?.length && obj.objectives) || []; + } +} + export class PutFundingData { fundingSources: PostFundingSource[]; diff --git a/api/src/models/project-view.test.ts b/api/src/models/project-view.test.ts index 59a9f842..599c9ad1 100644 --- a/api/src/models/project-view.test.ts +++ b/api/src/models/project-view.test.ts @@ -5,6 +5,7 @@ import { GetFundingData, GetIUCNClassificationData, GetLocationData, + GetObjectivesData, GetPartnershipsData, GetPermitData, GetProjectData, @@ -65,6 +66,60 @@ describe('GetPartnershipsData', () => { }); }); +describe('GetObjectivesData', () => { + describe('No values provided', () => { + let data: GetObjectivesData; + + before(() => { + data = new GetObjectivesData(null as unknown as any[]); + }); + + it('sets objectives', function () { + expect(data.objectives).to.eql([]); + }); + }); + + describe('Empty arrays as values provided', () => { + let data: GetObjectivesData; + + before(() => { + data = new GetObjectivesData([]); + }); + + it('sets objectives', function () { + expect(data.objectives).to.eql([]); + }); + }); + + describe('objectives values provided', () => { + let data: GetObjectivesData; + + const objectives = [{ objective: 'objective 1' }, { objective: 'objective 2' }]; + + before(() => { + data = new GetObjectivesData(objectives); + }); + + it('sets objectives', function () { + expect(data.objectives).to.eql(['objective 1', 'objective 2']); + }); + }); + + describe('All values provided', () => { + let data: GetObjectivesData; + + const objectives = [{ objective: 'objective 3' }, { objective: 'objective 4' }]; + + before(() => { + data = new GetObjectivesData(objectives); + }); + + it('sets objectives', function () { + expect(data.objectives).to.eql(['objective 3', 'objective 4']); + }); + }); +}); + describe('GetIUCNClassificationData', () => { describe('No values provided', () => { it('sets classification details', function () { diff --git a/api/src/models/project-view.ts b/api/src/models/project-view.ts index 29b68592..0ea395a9 100644 --- a/api/src/models/project-view.ts +++ b/api/src/models/project-view.ts @@ -7,6 +7,7 @@ export type ProjectObject = { contact: GetContactData; permit: GetPermitData; partnerships: GetPartnershipsData; + objectives: GetObjectivesData; funding: GetFundingData; location: GetLocationData; }; @@ -129,6 +130,17 @@ export class GetPartnershipsData { } } +export interface IGetObjective { + objective: string; +} +export class GetObjectivesData { + objectives: IGetObjective[]; + + constructor(objectives?: any[]) { + this.objectives = (objectives?.length && objectives.map((item: any) => item.objective)) || []; + } +} + export class GetLocationData { geometry?: Feature[]; is_within_overlapping?: string; diff --git a/api/src/openapi/schemas/project.ts b/api/src/openapi/schemas/project.ts index 39c4635e..2f9f51a9 100644 --- a/api/src/openapi/schemas/project.ts +++ b/api/src/openapi/schemas/project.ts @@ -166,6 +166,24 @@ export const projectCreatePostRequestObject = { } } } + }, + objective: { + title: 'Project objectives', + type: 'object', + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } } } }; @@ -200,7 +218,8 @@ const projectUpdateProperties = { } }, funding: { type: 'object', properties: {} }, - partnership: { type: 'object', properties: {} } + partnership: { type: 'object', properties: {} }, + objective: { type: 'object', properties: {} } }; /** diff --git a/api/src/paths/project/create.ts b/api/src/paths/project/create.ts index 0233da75..33ec8def 100644 --- a/api/src/paths/project/create.ts +++ b/api/src/paths/project/create.ts @@ -113,14 +113,12 @@ POST.apiDoc = { objective: { title: 'Project objectives', type: 'object', - required: ['objectives'], additionalProperties: false, properties: { objectives: { type: 'array', - required: ['objective'], items: { - title: 'Project objective', + title: 'Project objectives', type: 'object', properties: { objective: { diff --git a/api/src/paths/project/list.ts b/api/src/paths/project/list.ts index 47c8d149..b81d724d 100644 --- a/api/src/paths/project/list.ts +++ b/api/src/paths/project/list.ts @@ -445,6 +445,25 @@ GET.apiDoc = { } } }, + objective: { + title: 'Project objectives', + type: 'object', + required: ['objectives'], + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } + }, location: { description: 'The project location object', type: 'object', diff --git a/api/src/paths/project/{projectId}/update.test.ts b/api/src/paths/project/{projectId}/update.test.ts index c2da3ec2..4a83556b 100644 --- a/api/src/paths/project/{projectId}/update.test.ts +++ b/api/src/paths/project/{projectId}/update.test.ts @@ -106,6 +106,7 @@ describe('update', () => { permit: {}, funding: {}, partnerships: {}, + objectives: {}, location: {} }; diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index 2fc81465..9aa3ded9 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -251,6 +251,26 @@ PUT.apiDoc = { } } }, + objective: { + description: 'Project objectives', + type: 'object', + required: ['objectives'], + additionalProperties: false, + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } + }, location: { description: 'The project location object', type: 'object', @@ -327,6 +347,7 @@ export interface IUpdateProject { iucn: object | null; funding: object | null; partnership: object | null; + objective: object | null; } /** diff --git a/api/src/paths/project/{projectId}/view.ts b/api/src/paths/project/{projectId}/view.ts index a0ba217a..a7c2c4bb 100644 --- a/api/src/paths/project/{projectId}/view.ts +++ b/api/src/paths/project/{projectId}/view.ts @@ -261,6 +261,25 @@ GET.apiDoc = { } } }, + objective: { + description: 'Project objectives', + type: 'object', + required: ['objectives'], + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } + }, location: { description: 'The project location object', type: 'object', diff --git a/api/src/paths/public/project/{projectId}/view.ts b/api/src/paths/public/project/{projectId}/view.ts index 2c771606..d509d54c 100644 --- a/api/src/paths/public/project/{projectId}/view.ts +++ b/api/src/paths/public/project/{projectId}/view.ts @@ -218,6 +218,25 @@ GET.apiDoc = { } } }, + objective: { + description: 'Project objectives', + type: 'object', + required: ['objectives'], + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } + }, location: { description: 'The project location object', type: 'object', diff --git a/api/src/paths/public/projects.ts b/api/src/paths/public/projects.ts index 32991111..605d6a21 100644 --- a/api/src/paths/public/projects.ts +++ b/api/src/paths/public/projects.ts @@ -386,6 +386,25 @@ GET.apiDoc = { } } }, + objective: { + description: 'Project objectives', + type: 'object', + required: ['objectives'], + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } + }, location: { description: 'The project location object', type: 'object', diff --git a/api/src/paths/user/{userId}/projects/list.ts b/api/src/paths/user/{userId}/projects/list.ts index c5adb500..71400bbc 100644 --- a/api/src/paths/user/{userId}/projects/list.ts +++ b/api/src/paths/user/{userId}/projects/list.ts @@ -242,6 +242,25 @@ GET.apiDoc = { } } }, + objective: { + description: 'Project objectives', + type: 'object', + required: ['objectives'], + properties: { + objectives: { + type: 'array', + items: { + title: 'Project objectives', + type: 'object', + properties: { + objective: { + type: 'string' + } + } + } + } + } + }, location: { description: 'The project location object', type: 'object', diff --git a/api/src/queries/project/project-delete-queries.test.ts b/api/src/queries/project/project-delete-queries.test.ts index c9c385ac..0e2348e5 100644 --- a/api/src/queries/project/project-delete-queries.test.ts +++ b/api/src/queries/project/project-delete-queries.test.ts @@ -3,6 +3,7 @@ import { describe } from 'mocha'; import { deleteContactSQL, deleteIUCNSQL, + deleteObjectivesSQL, deletePartnershipsSQL, deletePermitSQL, deleteProjectFundingSourceSQL, @@ -55,6 +56,20 @@ describe('deletePartnershipsSQL', () => { }); }); +describe('deleteObjectivesSQL', () => { + it('returns null response when null projectId provided', () => { + const response = deleteObjectivesSQL(null as unknown as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId provided', () => { + const response = deleteObjectivesSQL(1); + + expect(response).to.not.be.null; + }); +}); + describe('deleteProjectSQL', () => { it('returns null response when null projectId provided', () => { const response = deleteProjectSQL(null as unknown as number); diff --git a/api/src/queries/project/project-delete-queries.ts b/api/src/queries/project/project-delete-queries.ts index 2b0cd5d2..2757f20d 100644 --- a/api/src/queries/project/project-delete-queries.ts +++ b/api/src/queries/project/project-delete-queries.ts @@ -105,6 +105,40 @@ export const deletePartnershipsSQL = (projectId: number): SQLStatement | null => return sqlStatement; }; +/** + * SQL query to delete project objective rows + * + * @param {projectId} projectId + * @returns {SQLStatement} sql query object + */ +export const deleteObjectivesSQL = (projectId: number): SQLStatement | null => { + defaultLog.debug({ + label: 'deleteObjectivesSQL', + message: 'params', + projectId + }); + + if (!projectId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + DELETE + from objective + WHERE + project_id = ${projectId}; + `; + + defaultLog.debug({ + label: 'deleteObjectivesSQL', + message: 'sql', + 'sqlStatement.text': sqlStatement.text, + 'sqlStatement.values': sqlStatement.values + }); + + return sqlStatement; +}; + /** * SQL query to delete project IUCN rows. * diff --git a/api/src/services/project-service.test.ts b/api/src/services/project-service.test.ts index 5c4ea711..d85174f9 100644 --- a/api/src/services/project-service.test.ts +++ b/api/src/services/project-service.test.ts @@ -21,6 +21,7 @@ const entitiesInitValue = { contact: null, authorization: null, partnership: null, + objective: null, iucn: null, funding: null, location: null, @@ -563,7 +564,7 @@ describe.skip('ProjectService', () => { await projectService.getPartnershipsData(projectId); expect.fail(); } catch (actualError) { - expect((actualError as HTTPError).message).to.equal('Failed to get partnership data'); + expect((actualError as HTTPError).message).to.equal('Failed to get partnerships data'); expect((actualError as HTTPError).status).to.equal(400); } }); @@ -584,6 +585,45 @@ describe.skip('ProjectService', () => { }); }); + describe('getObjectivesData', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 response when getObjectivesRows response has no rowCount', async () => { + const mockDBConnection = getMockDBConnection(); + + const projectService = new ProjectService(mockDBConnection); + + sinon.stub(projectService, 'getObjectivesRows').resolves(); + + const projectId = 1; + + try { + await projectService.getObjectivesData(projectId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to get objectives data'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('returns row on success', async () => { + const mockRowObj = [{ project_id: 1 }]; + + const mockQueryResponse = { rows: mockRowObj } as unknown as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getObjectivesData(projectId); + + expect(result).to.deep.include(new projectViewModels.GetObjectivesData(mockRowObj)); + }); + }); + describe('getFundingData', () => { afterEach(() => { sinon.restore(); @@ -755,6 +795,7 @@ describe.skip('ProjectService', () => { funding: new projectCreateModels.PostFundingData(), iucn: new projectCreateModels.PostIUCNData(), partnership: new projectCreateModels.PostPartnershipsData(), + objective: new projectCreateModels.PostObjectivesData(), authorization: new projectCreateModels.PostAuthorizationData(), focus: new projectCreateModels.PostFocusData(), restoration_plan: new projectCreateModels.PostRestPlanData() @@ -829,6 +870,7 @@ describe.skip('ProjectService', () => { ] }, partnership: { partnerships: [{ partnership: 'Canada Nature Fund' }, { partnership: 'BC Parks Living Labs' }] }, + objective: { objectives: [{ objective: 'This is objective 1' }, { objective: 'This is objective 2' }] }, location: { geometry: [{} as unknown as Feature], is_within_overlapping: 'string', @@ -1123,6 +1165,45 @@ describe.skip('ProjectService', () => { }); }); + describe('insertObjective', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 response when response has no id', async () => { + const mockQueryResponse = { noId: true } as unknown as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const objective = 'something'; + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.insertObjective(objective, projectId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to insert project objective data'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('returns id on success', async () => { + const mockRowObj = [{ id: 1 }]; + const mockQueryResponse = { rows: mockRowObj } as unknown as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const objective = 'something'; + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.insertObjective(objective, projectId); + + expect(result).equals(mockRowObj[0].id); + }); + }); + describe('insertPermit', () => { afterEach(() => { sinon.restore(); @@ -1558,6 +1639,7 @@ describe.skip('ProjectService', () => { expect(projectServiceSpy.updateContactData).not.to.have.been.called; expect(projectServiceSpy.updateProjectIUCNData).not.to.have.been.called; expect(projectServiceSpy.updateProjectPartnershipsData).not.to.have.been.called; + expect(projectServiceSpy.updateProjectObjectivesData).not.to.have.been.called; expect(projectServiceSpy.updateProjectFundingData).not.to.have.been.called; expect(projectServiceSpy.updateProjectSpatialData).not.to.have.been.called; expect(projectServiceSpy.updateProjectRegionData).not.to.have.been.called; @@ -1573,6 +1655,7 @@ describe.skip('ProjectService', () => { contact: new projectCreateModels.PostContactData(), authorization: new projectCreateModels.PostAuthorizationData(), partnership: new projectUpdateModels.PutPartnershipsData(), + objective: new projectUpdateModels.PutObjectivesData(), iucn: new projectUpdateModels.PutIUCNData(), funding: new projectUpdateModels.PutFundingData(), location: new projectUpdateModels.PutLocationData(), @@ -1590,6 +1673,7 @@ describe.skip('ProjectService', () => { expect(projectServiceSpy.updateContactData).to.have.been.called; expect(projectServiceSpy.updateProjectIUCNData).to.have.been.called; expect(projectServiceSpy.updateProjectPartnershipsData).to.have.been.called; + expect(projectServiceSpy.updateProjectObjectivesData).to.have.been.called; expect(projectServiceSpy.updateProjectFundingData).to.have.been.called; expect(projectServiceSpy.updateProjectSpatialData).to.have.been.called; expect(projectServiceSpy.updateProjectRegionData).to.have.been.called; @@ -1950,6 +2034,90 @@ describe.skip('ProjectService', () => { }); }); + describe('updateProjectObjectivesData', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 response when no sql statement produced for objectives', async () => { + const mockQuery = sinon.stub().onCall(0).returns(Promise.resolve([])).onCall(1).returns(Promise.resolve([])); + + const mockDBConnection = getMockDBConnection({ + query: mockQuery + }); + + sinon.stub(queries.project, 'deleteObjectivesSQL').returns(null); + + const projectId = 1; + const entities: IUpdateProject = { + ...entitiesInitValue, + objective: new projectCreateModels.PostObjectivesData() + }; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.updateProjectObjectivesData(projectId, entities); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 409 response when delete objectives fails', async () => { + const mockQuery = sinon.stub().onCall(0).returns(Promise.resolve([])).onCall(1).returns(Promise.resolve(null)); + + const mockDBConnection = getMockDBConnection({ + query: mockQuery + }); + + const projectId = 1; + const entities: IUpdateProject = { + ...entitiesInitValue, + objective: new projectCreateModels.PostObjectivesData() + }; + + sinon.stub(queries.project, 'deleteObjectivesSQL').returns(SQL`valid sql`); + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.updateProjectObjectivesData(projectId, entities); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to delete project objectives data'); + expect((actualError as HTTPError).status).to.equal(409); + } + }); + + it('should insert the new objectives information', async () => { + const mockQuery = sinon.stub().onCall(0).returns(Promise.resolve([])).onCall(1).returns(Promise.resolve([])); + + const mockDBConnection = getMockDBConnection({ + query: mockQuery + }); + + const projectId = 1; + const entities: IUpdateProject = { + ...entitiesInitValue, + objective: { + objectives: ['objective1', 'objective2'] + } + }; + + sinon.stub(queries.project, 'deleteObjectivesSQL').returns(SQL`valid sql`); + + const insertObjectiveStub = sinon.stub(ProjectService.prototype, 'insertObjective').resolves(1); + + const projectService = new ProjectService(mockDBConnection); + + await projectService.updateProjectObjectivesData(projectId, entities); + + expect(insertObjectiveStub).to.have.been.calledTwice; + }); + }); + describe('updateProjectFundingData', () => { afterEach(() => { sinon.restore(); diff --git a/api/src/services/project-service.ts b/api/src/services/project-service.ts index f59d557c..a34ce14e 100644 --- a/api/src/services/project-service.ts +++ b/api/src/services/project-service.ts @@ -7,6 +7,7 @@ import { IPostAuthorization, IPostContact, IPostIUCN, + IPostObjective, IPostPartnership, PostFocusData, PostFundingSource, @@ -20,6 +21,7 @@ import { GetFundingData, GetIUCNClassificationData, GetLocationData, + GetObjectivesData, GetPartnershipsData, GetPermitData, GetProjectData, @@ -149,17 +151,27 @@ export class ProjectService extends DBService { * @memberof ProjectService */ async getProjectById(projectId: number, isPublic = false): Promise { - const [projectData, speciesData, iucnData, contactData, permitData, partnershipsData, fundingData, locationData] = - await Promise.all([ - this.getProjectData(projectId), - this.getSpeciesData(projectId), - this.getIUCNClassificationData(projectId), - this.getContactData(projectId, isPublic), - this.getPermitData(projectId, isPublic), - this.getPartnershipsData(projectId), - this.getFundingData(projectId), - this.getLocationData(projectId) - ]); + const [ + projectData, + speciesData, + iucnData, + contactData, + permitData, + partnershipsData, + objectivesData, + fundingData, + locationData + ] = await Promise.all([ + this.getProjectData(projectId), + this.getSpeciesData(projectId), + this.getIUCNClassificationData(projectId), + this.getContactData(projectId, isPublic), + this.getPermitData(projectId, isPublic), + this.getPartnershipsData(projectId), + this.getObjectivesData(projectId), + this.getFundingData(projectId), + this.getLocationData(projectId) + ]); return { project: projectData, @@ -168,6 +180,7 @@ export class ProjectService extends DBService { contact: contactData, permit: permitData, partnerships: partnershipsData, + objectives: objectivesData, funding: fundingData, location: locationData }; @@ -326,7 +339,7 @@ export class ProjectService extends DBService { const [partnershipsRows] = await Promise.all([this.getPartnershipsRows(projectId)]); if (!partnershipsRows) { - throw new HTTP400('Failed to get partnership data'); + throw new HTTP400('Failed to get partnerships data'); } return new GetPartnershipsData(partnershipsRows); @@ -347,6 +360,31 @@ export class ProjectService extends DBService { return (response && response.rows) || null; } + async getObjectivesData(projectId: number): Promise { + const [objectivesRows] = await Promise.all([this.getObjectivesRows(projectId)]); + + if (!objectivesRows) { + throw new HTTP400('Failed to get objectives data'); + } + + return new GetObjectivesData(objectivesRows); + } + + async getObjectivesRows(projectId: number): Promise { + const sqlStatement = SQL` + SELECT + objective + FROM + objective + WHERE + project_id = ${projectId}; + `; + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + return (response && response.rows) || null; + } + async getFundingData(projectId: number): Promise { const sqlStatement = SQL` SELECT @@ -501,7 +539,7 @@ export class ProjectService extends DBService { ) ); - // Handle partners + // Handle partnerships promises.push( Promise.all( postProjectData.partnership.partnerships?.map((partner: IPostPartnership) => @@ -510,6 +548,15 @@ export class ProjectService extends DBService { ) ); + // Handle objectives + promises.push( + Promise.all( + postProjectData.objective.objectives?.map((objective: IPostObjective) => + this.insertObjective(objective.objective, projectId) + ) || [] + ) + ); + // Handle new project authorizations promises.push( Promise.all( @@ -688,6 +735,30 @@ export class ProjectService extends DBService { return result.id; } + async insertObjective(objective: string, projectId: number): Promise { + const sqlStatement = SQL` + INSERT INTO objective ( + project_id, + objective + ) VALUES ( + ${projectId}, + ${objective} + ) + RETURNING + objective_id as id; + `; + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project objective data'); + } + + return result.id; + } + async insertPermit(permitNumber: string, permitType: string, projectId: number): Promise { const systemUserId = this.connection.systemUserId(); @@ -823,6 +894,10 @@ export class ProjectService extends DBService { promises.push(this.updateProjectPartnershipsData(projectId, entities)); } + if (entities?.objective) { + promises.push(this.updateProjectObjectivesData(projectId, entities)); + } + if (entities?.iucn) { promises.push(this.updateProjectIUCNData(projectId, entities)); } @@ -941,6 +1016,32 @@ export class ProjectService extends DBService { await Promise.all([...insertPartnershipsPromises]); } + async updateProjectObjectivesData(projectId: number, entities: IUpdateProject): Promise { + const putObjectivesData = (entities?.objective && new models.project.PutObjectivesData(entities.objective)) || null; + + const sqlDeleteObjectivesStatement = queries.project.deleteObjectivesSQL(projectId); + + if (!sqlDeleteObjectivesStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const deleteObjectivesPromises = this.connection.query( + sqlDeleteObjectivesStatement.text, + sqlDeleteObjectivesStatement.values + ); + + const [deleteObjectivesResult] = await Promise.all([deleteObjectivesPromises]); + + if (!deleteObjectivesResult) { + throw new HTTP409('Failed to delete project objectives data'); + } + + const insertObjectivesPromises = + putObjectivesData?.objectives?.map((objective: string) => this.insertObjective(objective, projectId)) || []; + + await Promise.all([...insertObjectivesPromises]); + } + async updateProjectFundingData(projectId: number, entities: IUpdateProject): Promise { const putFundingSources = entities?.funding && new models.project.PutFundingData(entities.funding); @@ -1053,6 +1154,7 @@ export class ProjectService extends DBService { * contact: GetContactData; * permit: GetPermitData; * partnerships: GetPartnershipsData; + * objectives: GetObjectivesData; * funding: GetFundingData; * location: GetLocationData; * }[] @@ -1070,6 +1172,7 @@ export class ProjectService extends DBService { contact: GetContactData; permit: GetPermitData; partnerships: GetPartnershipsData; + objectives: GetObjectivesData; funding: GetFundingData; location: GetLocationData; }[] diff --git a/app/src/features/projects/components/ProjectObjectivesForm.tsx b/app/src/features/projects/components/ProjectObjectivesForm.tsx index 41816c5f..3ae5e4cc 100644 --- a/app/src/features/projects/components/ProjectObjectivesForm.tsx +++ b/app/src/features/projects/components/ProjectObjectivesForm.tsx @@ -57,13 +57,16 @@ export const ProjectObjectiveFormYupSchema = yup.object().shape({ objective: yup.object().shape({ objectives: yup .array() - // .of( - // yup.object().shape({ - // objective: yup.string().required('Objective required') - // }) - // ) - .min(1, 'At least one objective required') - .required('required') + .of( + yup.object().shape({ + objective: yup + .string() + .max(300, 'Cannot exceed 500 characters') + .trim() + .required('Please enter an objective') + }) + ) + .isUniqueObjective('Objective entries must be unique') }) }); @@ -127,8 +130,9 @@ const ProjectObjectivesForm: React.FC = () => {