diff --git a/api/apps/api/src/modules/projects/projects-crud.service.ts b/api/apps/api/src/modules/projects/projects-crud.service.ts index c573f05ec6..b12f2c4b79 100644 --- a/api/apps/api/src/modules/projects/projects-crud.service.ts +++ b/api/apps/api/src/modules/projects/projects-crud.service.ts @@ -16,6 +16,8 @@ import { 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'; +import { GeoFeature } from '@marxan-api/modules/geo-features/geo-feature.api.entity'; +import { User } from '@marxan-api/modules/users/user.api.entity'; import { FetchSpecification } from 'nestjs-base-service'; import { MultiplePlanningAreaIds, @@ -35,6 +37,12 @@ type ProjectFilterKeys = keyof Pick< >; type ProjectFilters = Record; +export type ProjectsInfoDTO = AppInfoDTO & { + params?: { + nameSearch?: string; + }; +}; + @Injectable() export class ProjectsCrudService extends AppBaseService< Project, @@ -233,6 +241,36 @@ export class ProjectsCrudService extends AppBaseService< return entity; } + async extendFindAllQuery( + query: SelectQueryBuilder, + fetchSpecification: FetchSpecification, + info?: ProjectsInfoDTO, + ): Promise> { + const { namesSearch } = info?.params ?? {}; + if (namesSearch) { + const nameSearchFilterField = 'nameSearchFilter' as const; + query.leftJoin( + GeoFeature, + 'geofeature', + `${this.alias}.id = geofeature.project_id`, + ); + query.leftJoin(User, 'user', `${this.alias}.createdBy = user.id`); + query.andWhere( + `( + ${this.alias}.name + ||' '|| COALESCE(geofeature.description, '') + ||' '|| COALESCE(geofeature.feature_class_name, '') + ||' '|| COALESCE(user.fname, '') + ||' '|| COALESCE(user.lname, '') + ||' '|| COALESCE(user.display_name, '') + ) ILIKE :${nameSearchFilterField}`, + { [nameSearchFilterField]: `%${namesSearch}%` }, + ); + } + + return query; + } + async extendFindAllResults( entitiesAndCount: [Project[], number], _fetchSpecification?: FetchSpecification, diff --git a/api/apps/api/src/modules/projects/projects.controller.ts b/api/apps/api/src/modules/projects/projects.controller.ts index a8f90049bf..211ac79263 100644 --- a/api/apps/api/src/modules/projects/projects.controller.ts +++ b/api/apps/api/src/modules/projects/projects.controller.ts @@ -28,6 +28,7 @@ import { ApiNoContentResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; @@ -128,11 +129,21 @@ export class ProjectsController { { name: 'adminAreaLevel21Id' }, ], }) + @ApiQuery({ + name: 'q', + required: false, + description: `A free search over names`, + }) @Get() async findAll( @ProcessFetchSpecification() fetchSpecification: FetchSpecification, + @Query('q') namesSearch?: string, ): Promise { - const results = await this.projectsService.findAll(fetchSpecification); + const results = await this.projectsService.findAll(fetchSpecification, { + params: { + namesSearch, + }, + }); return this.projectSerializer.serialize(results.data, results.metadata); } diff --git a/api/apps/api/src/modules/projects/projects.service.ts b/api/apps/api/src/modules/projects/projects.service.ts index 5bbdb0de7e..99e3161733 100644 --- a/api/apps/api/src/modules/projects/projects.service.ts +++ b/api/apps/api/src/modules/projects/projects.service.ts @@ -4,7 +4,7 @@ import { AppInfoDTO } from '@marxan-api/dto/info.dto'; import { GeoFeaturesService } from '@marxan-api/modules/geo-features/geo-features.service'; -import { ProjectsCrudService } from './projects-crud.service'; +import { ProjectsCrudService, ProjectsInfoDTO } from './projects-crud.service'; import { JobStatusService } from './job-status'; import { ProtectedAreasFacade } from './protected-areas/protected-areas.facade'; import { Project } from './project.api.entity'; @@ -32,9 +32,9 @@ export class ProjectsService { return this.geoCrud.findAllPaginated(fetchSpec, appInfo); } - async findAll(fetchSpec: FetchSpecification, _?: AppInfoDTO) { + async findAll(fetchSpec: FetchSpecification, info?: ProjectsInfoDTO) { // /ACL slot/ - return this.projectsCrud.findAllPaginated(fetchSpec); + return this.projectsCrud.findAllPaginated(fetchSpec, info); } async findOne(id: string) { diff --git a/api/apps/api/test/projects.e2e-spec.ts b/api/apps/api/test/projects.e2e-spec.ts index 8db5cf5ef7..a34d9c5a7c 100644 --- a/api/apps/api/test/projects.e2e-spec.ts +++ b/api/apps/api/test/projects.e2e-spec.ts @@ -124,6 +124,17 @@ describe('ProjectsModule (e2e)', () => { expect(jsonAPIResponse.data[0].type).toBe('projects'); }); + test('A user should be able to get a list of projects with q param', async () => { + const response = await request(app.getHttpServer()) + .get('/api/v1/projects?q=User') + .set('Authorization', `Bearer ${jwtToken}`) + .expect(200); + + const jsonAPIResponse: ProjectResultPlural = response.body; + + expect(jsonAPIResponse.data[0].type).toBe('projects'); + }); + test('A user should be get a list of projects without any included relationships if these have not been requested', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/projects')