Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): projects: deny uploading shapefile for non-existing project #218

Merged
merged 7 commits into from
Jun 1, 2021
16 changes: 16 additions & 0 deletions api/apps/api/src/modules/projects/dto/geo-feature.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { GeoFeaturesService } from '@marxan-api/modules/geo-features/geo-features.service';
import { GeoFeature } from '@marxan-api/modules/geo-features/geo-feature.api.entity';
import { PaginationMeta } from '@marxan-api/utils/app-base.service';

@Injectable()
export class GeoFeatureSerializer {
constructor(private readonly geoFeaturesService: GeoFeaturesService) {}

async serialize(
entities: Partial<GeoFeature> | (Partial<GeoFeature> | undefined)[],
paginationMeta?: PaginationMeta,
): Promise<any> {
return this.geoFeaturesService.serialize(entities, paginationMeta);
}
}
16 changes: 16 additions & 0 deletions api/apps/api/src/modules/projects/dto/project.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { PaginationMeta } from '@marxan-api/utils/app-base.service';
import { ProjectsCrudService } from '../projects-crud.service';
import { Project } from '../project.api.entity';

@Injectable()
export class ProjectSerializer {
constructor(private readonly projectsCrud: ProjectsCrudService) {}

async serialize(
entities: Partial<Project> | (Partial<Project> | undefined)[],
paginationMeta?: PaginationMeta,
): Promise<any> {
return this.projectsCrud.serialize(entities, paginationMeta);
}
}
202 changes: 202 additions & 0 deletions api/apps/api/src/modules/projects/projects-crud.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AppInfoDTO } from '@marxan-api/dto/info.dto';
import { Repository, SelectQueryBuilder } from 'typeorm';
import { Project } from './project.api.entity';
import { CreateProjectDTO } from './dto/create.project.dto';
import { UpdateProjectDTO } from './dto/update.project.dto';
import { UsersService } from '@marxan-api/modules/users/users.service';
import { ScenariosService } from '@marxan-api/modules/scenarios/scenarios.service';
import { PlanningUnitsService } from '@marxan-api/modules/planning-units/planning-units.service';
import {
AppBaseService,
JSONAPISerializerConfig,
} from '@marxan-api/utils/app-base.service';
import { Country } from '@marxan-api/modules/countries/country.geo.entity';
import { AdminArea } from '@marxan-api/modules/admin-areas/admin-area.geo.entity';
import { AdminAreasService } from '@marxan-api/modules/admin-areas/admin-areas.service';
import { CountriesService } from '@marxan-api/modules/countries/countries.service';
import { AppConfig } from '@marxan-api/utils/config.utils';

const projectFilterKeyNames = [
'name',
'organizationId',
'countryId',
'adminAreaLevel1Id',
'adminAreaLevel2Id',
] as const;
type ProjectFilterKeys = keyof Pick<
Project,
typeof projectFilterKeyNames[number]
>;
type ProjectFilters = Record<ProjectFilterKeys, string[]>;

@Injectable()
export class ProjectsCrudService extends AppBaseService<
Project,
CreateProjectDTO,
UpdateProjectDTO,
AppInfoDTO
> {
constructor(
@InjectRepository(Project)
protected readonly repository: Repository<Project>,
@Inject(forwardRef(() => ScenariosService))
protected readonly scenariosService: ScenariosService,
@Inject(UsersService) protected readonly usersService: UsersService,
@Inject(AdminAreasService)
protected readonly adminAreasService: AdminAreasService,
@Inject(CountriesService)
protected readonly countriesService: CountriesService,
@Inject(PlanningUnitsService)
private readonly planningUnitsService: PlanningUnitsService,
) {
super(repository, 'project', 'projects', {
logging: { muteAll: AppConfig.get<boolean>('logging.muteAll', false) },
});
}

get serializerConfig(): JSONAPISerializerConfig<Project> {
return {
attributes: [
'name',
'description',
'countryId',
'adminAreaLevel1Id',
'adminAreaLevel2Id',
'planningUnitGridShape',
'planningUnitAreakm2',
'users',
'scenarios',
'createdAt',
'lastModifiedAt',
],
keyForAttribute: 'camelCase',
users: {
ref: 'id',
attributes: ['fname', 'lname', 'email', 'projectRoles'],
projectRoles: {
ref: 'name',
attributes: ['name'],
},
},
scenarios: {
ref: 'id',
attributes: [
'name',
'description',
'type',
'wdpaFilter',
'wdpaThreshold',
'adminRegionId',
'numberOfRuns',
'boundaryLengthModifier',
'metadata',
'status',
'createdAt',
'lastModifiedAt',
],
},
};
}

/**
* Apply service-specific filters.
*/
setFilters(
query: SelectQueryBuilder<Project>,
filters: ProjectFilters,
_info?: AppInfoDTO,
): SelectQueryBuilder<Project> {
this._processBaseFilters<ProjectFilters>(
query,
filters,
projectFilterKeyNames,
);
return query;
}

async setDataCreate(
create: CreateProjectDTO,
info?: AppInfoDTO,
): Promise<Project> {
/**
* @debt Temporary setup. I think we should remove TimeUserEntityMetadata
* from entities and just use a separate event log, and a view to obtain the
* same information (who created an entity and when, and when it was last
* modified) from that log, kind of event sourcing way.
*/
const project = await super.setDataCreate(create, info);
project.createdBy = info?.authenticatedUser?.id!;
return project;
}

/**
* Look up the planning area for this project.
*
* In decreasing precedence (i.e. most specific is used):
*
* * a project-specific protected area (@todo not implemented yet)
* * a level 2 admin area
* * a level 1 admin area
* * a country
*/
async getPlanningArea(
project: Partial<Project>,
): Promise<Country | Partial<AdminArea | undefined>> {
const planningArea = project.planningAreaGeometryId
? /**
* @todo here we should look up the actual custom planning area from
* `planningAreaGeometryId`, when we implement this.
*/
new AdminArea()
: project.adminAreaLevel2Id
? await this.adminAreasService.getByLevel1OrLevel2Id(
project.adminAreaLevel2Id!,
)
: project.adminAreaLevel1Id
? await this.adminAreasService.getByLevel1OrLevel2Id(
project.adminAreaLevel1Id!,
)
: project.countryId
? await this.countriesService.getById(project.countryId)
: undefined;
return planningArea;
}

async actionAfterCreate(
model: Project,
createModel: CreateProjectDTO,
_info?: AppInfoDTO,
): Promise<void> {
if (
createModel.planningUnitAreakm2 &&
createModel.planningUnitGridShape &&
(createModel.countryId ||
createModel.adminAreaLevel1Id ||
createModel.adminAreaLevel2Id ||
createModel.extent)
) {
this.logger.debug('creating planning unit job ');
return this.planningUnitsService.create(createModel);
}
}

async actionAfterUpdate(
model: Project,
createModel: UpdateProjectDTO,
_info?: AppInfoDTO,
): Promise<void> {
if (
createModel.planningUnitAreakm2 &&
createModel.planningUnitGridShape &&
(createModel.countryId ||
createModel.adminAreaLevel1Id ||
createModel.adminAreaLevel2Id ||
createModel.extent)
) {
this.logger.debug('creating planning unit job ');
return this.planningUnitsService.create(createModel);
}
}
}
Loading