-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(api): add service for jobs status
- Loading branch information
Showing
9 changed files
with
343 additions
and
25 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
api/apps/api/src/migrations/api/1623311716713-AddJobStatusView.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
||
export class AddJobStatusView1623311716713 implements MigrationInterface { | ||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(` | ||
CREATE VIEW "scenario_job_status" AS | ||
SELECT | ||
DISTINCT ON ("jobType", topic) "jobType", | ||
api_events.topic AS "scenarioId", | ||
projects.id AS "projectId", | ||
api_events.kind | ||
FROM | ||
api_events | ||
INNER JOIN scenarios ON api_events.topic = scenarios.id | ||
INNER JOIN projects ON projects.id = scenarios.project_id | ||
CROSS JOIN LATERAL SUBSTRING( | ||
api_events.kind | ||
FROM | ||
'scenario.#"[^.]*#"%' FOR '#' | ||
) AS "jobType" | ||
ORDER BY | ||
"jobType", | ||
api_events.topic, | ||
api_events.timestamp DESC; | ||
`); | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(` | ||
DROP VIEW "scenario_job_status"; | ||
`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
api/apps/api/src/modules/projects/job-status/job-status.view.api.entity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { ViewColumn, ViewEntity } from 'typeorm'; | ||
import { JobType } from '@marxan-api/modules/projects/job-status/jobs.enum'; | ||
import { JobStatus } from '@marxan-api/modules/scenarios/scenario.api.entity'; | ||
import { API_EVENT_KINDS } from '@marxan/api-events'; | ||
|
||
@ViewEntity({ | ||
expression: ` | ||
SELECT | ||
DISTINCT ON ("jobType", topic) "jobType", | ||
api_events.topic AS "scenarioId", | ||
projects.id AS "projectId", | ||
api_events.kind | ||
FROM | ||
api_events | ||
INNER JOIN scenarios ON api_events.topic = scenarios.id | ||
INNER JOIN projects ON projects.id = scenarios.project_id | ||
CROSS JOIN LATERAL SUBSTRING( | ||
api_events.kind | ||
FROM | ||
'scenario.#"[^.]*#"%' FOR '#' | ||
) AS "jobType" | ||
ORDER BY | ||
"jobType", | ||
api_events.topic, | ||
api_events.timestamp DESC; | ||
`, | ||
}) | ||
export class ScenarioJobStatus { | ||
@ViewColumn() | ||
jobType!: JobType; | ||
|
||
@ViewColumn() | ||
kind!: API_EVENT_KINDS; | ||
|
||
get jobStatus(): JobStatus | undefined { | ||
// I didn't use CASE ... THEN in the SQL as I wanted to enforce the compiler check on the mapping | ||
return eventToJobStatusMapping[this.kind]; | ||
} | ||
|
||
@ViewColumn() | ||
scenarioId!: string; | ||
|
||
@ViewColumn() | ||
projectId!: string; | ||
} | ||
|
||
const eventToJobStatusMapping: Record< | ||
API_EVENT_KINDS, | ||
JobStatus | undefined | ||
> = { | ||
[API_EVENT_KINDS.project__protectedAreas__failed__v1__alpha]: undefined, | ||
[API_EVENT_KINDS.project__protectedAreas__finished__v1__alpha]: undefined, | ||
[API_EVENT_KINDS.project__protectedAreas__submitted__v1__alpha]: undefined, | ||
[API_EVENT_KINDS.scenario__costSurface__costUpdateFailed__v1_alpha1]: | ||
JobStatus.failure, | ||
[API_EVENT_KINDS.scenario__costSurface__finished__v1_alpha1]: JobStatus.done, | ||
[API_EVENT_KINDS.scenario__costSurface__shapeConversionFailed__v1_alpha1]: | ||
JobStatus.failure, | ||
[API_EVENT_KINDS.scenario__costSurface__shapeConverted__v1_alpha1]: | ||
JobStatus.running, | ||
[API_EVENT_KINDS.scenario__costSurface__submitted__v1_alpha1]: | ||
JobStatus.running, | ||
[API_EVENT_KINDS.scenario__planningUnitsInclusion__failed__v1__alpha1]: | ||
JobStatus.failure, | ||
[API_EVENT_KINDS.scenario__planningUnitsInclusion__finished__v1__alpha1]: | ||
JobStatus.done, | ||
[API_EVENT_KINDS.scenario__planningUnitsInclusion__submitted__v1__alpha1]: | ||
JobStatus.running, | ||
[API_EVENT_KINDS.user__accountActivationFailed__v1alpha1]: undefined, | ||
[API_EVENT_KINDS.user__accountActivationSucceeded__v1alpha1]: undefined, | ||
[API_EVENT_KINDS.user__accountActivationTokenGenerated__v1alpha1]: undefined, | ||
[API_EVENT_KINDS.user__passwordResetFailed__v1alpha1]: undefined, | ||
[API_EVENT_KINDS.user__passwordResetSucceeded__v1alpha1]: undefined, | ||
[API_EVENT_KINDS.user__passwordResetTokenGenerated__v1alpha1]: undefined, | ||
[API_EVENT_KINDS.user__signedUp__v1alpha1]: undefined, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
api/apps/api/test/project-jobs-status/job-status.service.integration.e2e-spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { PromiseType } from 'utility-types'; | ||
import { Repository } from 'typeorm'; | ||
import { getRepositoryToken } from '@nestjs/typeorm'; | ||
import { ApiEventsService } from '@marxan-api/modules/api-events/api-events.service'; | ||
import { OrganizationsService } from '@marxan-api/modules/organizations/organizations.service'; | ||
import { ProjectsCrudService } from '@marxan-api/modules/projects/projects-crud.service'; | ||
import { ScenariosCrudService } from '@marxan-api/modules/scenarios/scenarios-crud.service'; | ||
import { Scenario as ScenarioEntity } from '@marxan-api/modules/scenarios/scenario.api.entity'; | ||
import { API_EVENT_KINDS } from '@marxan/api-events'; | ||
import { ScenarioJobStatus } from '@marxan-api/modules/projects/job-status/job-status.view.api.entity'; | ||
import { CreateOrganizationDTO } from '@marxan-api/modules/organizations/dto/create.organization.dto'; | ||
import { CreateProjectDTO } from '@marxan-api/modules/projects/dto/create.project.dto'; | ||
import { CreateScenarioDTO } from '@marxan-api/modules/scenarios/dto/create.scenario.dto'; | ||
import { Project } from '@marxan-api/modules/projects/project.api.entity'; | ||
import { ApiEvent } from '@marxan-api/modules/api-events/api-event.api.entity'; | ||
import { | ||
JobStatusService, | ||
Scenario, | ||
} from '@marxan-api/modules/projects/job-status'; | ||
import { E2E_CONFIG } from '../e2e.config'; | ||
import { bootstrapApplication } from '../utils/api-application'; | ||
|
||
let fixtures: PromiseType<ReturnType<typeof getFixtures>>; | ||
|
||
beforeEach(async () => { | ||
fixtures = await getFixtures(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await fixtures.cleanup(); | ||
}); | ||
|
||
describe(`when has two projects with scenarios and events`, () => { | ||
let result: Scenario[]; | ||
let scenarioIds: string[]; | ||
let projectId: string; | ||
beforeEach(async () => { | ||
({ | ||
scenarioIds, | ||
projectId, | ||
} = await fixtures.givenProjectWithTwoScenariosWithOverridingEvents()); | ||
await fixtures.givenAnotherProjectWithAScenarioWithAnEvent(); | ||
|
||
result = await fixtures.getJobStatusService().getJobStatusFor(projectId); | ||
}); | ||
|
||
it(`should return last events of every job for scenarios of the project`, () => { | ||
expect(result).toEqual( | ||
[ | ||
{ | ||
jobs: [ | ||
{ | ||
kind: 'costSurface', | ||
status: 'done', | ||
}, | ||
{ | ||
kind: 'planningUnitsInclusion', | ||
status: 'failure', | ||
}, | ||
], | ||
scenarioId: scenarioIds[0], | ||
}, | ||
{ | ||
jobs: [ | ||
{ | ||
kind: 'costSurface', | ||
status: 'running', | ||
}, | ||
], | ||
scenarioId: scenarioIds[1], | ||
}, | ||
].sort((a, b) => a.scenarioId.localeCompare(b.scenarioId)), | ||
); | ||
}); | ||
}); | ||
|
||
async function getFixtures() { | ||
const application = await bootstrapApplication(); | ||
|
||
const eventsRepository = application.get(ApiEventsService); | ||
const organizationRepository = application.get(OrganizationsService); | ||
const organization = await organizationRepository.create( | ||
E2E_CONFIG.organizations.valid.minimal() as CreateOrganizationDTO, | ||
); | ||
const projectRepository = application.get(ProjectsCrudService); | ||
const addedProjects: Project[] = []; | ||
const scenarioRepository = application.get(ScenariosCrudService); | ||
const addedScenarios: ScenarioEntity[] = []; | ||
|
||
const addedEvents: ApiEvent[] = []; | ||
|
||
const statusRepository: Repository<ScenarioJobStatus> = application.get( | ||
getRepositoryToken(ScenarioJobStatus), | ||
); | ||
|
||
const fixtures = { | ||
async givenProjectWithTwoScenariosWithOverridingEvents() { | ||
const project = await projectRepository.create({ | ||
...(E2E_CONFIG.projects.valid.minimal() as CreateProjectDTO), | ||
organizationId: organization.id, | ||
}); | ||
const scenario1 = await scenarioRepository.create({ | ||
...(E2E_CONFIG.scenarios.valid.minimal() as CreateScenarioDTO), | ||
projectId: project.id, | ||
}); | ||
const scenario2 = await scenarioRepository.create({ | ||
...(E2E_CONFIG.scenarios.valid.minimal() as CreateScenarioDTO), | ||
projectId: project.id, | ||
}); | ||
const eventDtos = [ | ||
{ | ||
kind: | ||
API_EVENT_KINDS.scenario__costSurface__costUpdateFailed__v1_alpha1, | ||
topic: scenario1.id, | ||
}, | ||
{ | ||
kind: API_EVENT_KINDS.scenario__costSurface__finished__v1_alpha1, | ||
topic: scenario1.id, | ||
}, | ||
{ | ||
kind: | ||
API_EVENT_KINDS.scenario__planningUnitsInclusion__submitted__v1__alpha1, | ||
topic: scenario1.id, | ||
}, | ||
{ | ||
kind: | ||
API_EVENT_KINDS.scenario__planningUnitsInclusion__failed__v1__alpha1, | ||
topic: scenario1.id, | ||
}, | ||
{ | ||
kind: API_EVENT_KINDS.scenario__costSurface__submitted__v1_alpha1, | ||
topic: scenario2.id, | ||
}, | ||
]; | ||
for (const eventDto of eventDtos) { | ||
const event = await eventsRepository.create(eventDto); | ||
addedEvents.push(event); | ||
} | ||
addedProjects.push(project); | ||
addedScenarios.push(scenario1, scenario2); | ||
return { | ||
projectId: project.id, | ||
scenarioIds: [scenario1.id, scenario2.id], | ||
}; | ||
}, | ||
async givenAnotherProjectWithAScenarioWithAnEvent() { | ||
const project = await projectRepository.create({ | ||
...(E2E_CONFIG.projects.valid.minimal() as CreateProjectDTO), | ||
organizationId: organization.id, | ||
}); | ||
const scenario = await scenarioRepository.create({ | ||
...(E2E_CONFIG.scenarios.valid.minimal() as CreateScenarioDTO), | ||
projectId: project.id, | ||
}); | ||
const event = await eventsRepository.create({ | ||
kind: | ||
API_EVENT_KINDS.scenario__planningUnitsInclusion__finished__v1__alpha1, | ||
topic: project.id, | ||
}); | ||
addedProjects.push(project); | ||
addedScenarios.push(scenario); | ||
addedEvents.push(event); | ||
}, | ||
async cleanup() { | ||
await eventsRepository.repo.remove(addedEvents); | ||
await Promise.all( | ||
addedScenarios.map((scenario) => | ||
scenarioRepository.remove(scenario.id), | ||
), | ||
); | ||
await Promise.all( | ||
addedProjects.map((project) => projectRepository.remove(project.id)), | ||
); | ||
await organizationRepository.remove(organization.id); | ||
}, | ||
getStatusRepository() { | ||
return statusRepository; | ||
}, | ||
getJobStatusService() { | ||
return application.get(JobStatusService); | ||
}, | ||
}; | ||
return fixtures; | ||
} |
Oops, something went wrong.