diff --git a/src/activities/entities/activity.entity.ts b/src/activities/entities/activity.entity.ts index d7cd90a..7b9d40a 100644 --- a/src/activities/entities/activity.entity.ts +++ b/src/activities/entities/activity.entity.ts @@ -1,4 +1,4 @@ -import { + import { Entity, PrimaryGeneratedColumn, Column, @@ -53,6 +53,10 @@ export class Activity extends BaseEntity { @Field() type: ActivityType; + @Column({ nullable: true }) + @Field({ nullable: true }) + customType: string; + @Column({ nullable: true }) @Field() name: string; diff --git a/src/activities/services/activity-routes.service.ts b/src/activities/services/activity-routes.service.ts index 0877cc3..52934b0 100644 --- a/src/activities/services/activity-routes.service.ts +++ b/src/activities/services/activity-routes.service.ts @@ -618,6 +618,12 @@ export class ActivityRoutesService { }); } + if (params.routeTypes != null) { + builder.andWhere('r.route_type_id IN(:...routeTypes)', { + routeTypes: params.routeTypes, + }); + } + if (params.publish != null) { builder.andWhere('ar."publish" IN (:...publish)', { publish: params.publish, diff --git a/src/crags/dtos/search-crags.input.ts b/src/crags/dtos/search-crags.input.ts new file mode 100644 index 0000000..44830ec --- /dev/null +++ b/src/crags/dtos/search-crags.input.ts @@ -0,0 +1,21 @@ +import { InputType, Field, Int } from '@nestjs/graphql'; +import { IsOptional } from 'class-validator'; +import { OrderByInput } from '../../core/interfaces/order-by-input.interface'; + +@InputType() +export class SearchCragsInput { + @Field() + query?: string; + + @Field({ nullable: true }) + @IsOptional() + orderBy?: OrderByInput; + + @Field(() => Int, { nullable: true }) + @IsOptional() + pageNumber?: number; + + @Field(() => Int, { nullable: true }) + @IsOptional() + pageSize?: number; +} diff --git a/src/crags/dtos/search-routes.ts b/src/crags/dtos/search-routes.ts new file mode 100644 index 0000000..d657b8e --- /dev/null +++ b/src/crags/dtos/search-routes.ts @@ -0,0 +1,25 @@ +import { InputType, Field, Int } from '@nestjs/graphql'; +import { IsOptional } from 'class-validator'; +import { OrderByInput } from '../../core/interfaces/order-by-input.interface'; + +@InputType() +export class SearchRoutesInput { + @Field() + query?: string; + + @Field({ nullable: true }) + @IsOptional() + cragId?: string; + + @Field({ nullable: true }) + @IsOptional() + orderBy?: OrderByInput; + + @Field(() => Int, { nullable: true }) + @IsOptional() + pageNumber?: number; + + @Field(() => Int, { nullable: true }) + @IsOptional() + pageSize?: number; +} diff --git a/src/crags/resolvers/search.resolver.ts b/src/crags/resolvers/search.resolver.ts index e54d238..0df91c8 100644 --- a/src/crags/resolvers/search.resolver.ts +++ b/src/crags/resolvers/search.resolver.ts @@ -8,6 +8,10 @@ import { DataLoaderInterceptor } from '../../core/interceptors/data-loader.inter import { User } from '../../users/entities/user.entity'; import { SearchService } from '../services/search.service'; import { SearchResults } from '../utils/search-results.class'; +import { SearchCragsInput } from '../dtos/search-crags.input'; +import { PaginatedCrags } from '../utils/paginated-crags'; +import { PaginatedRoutes } from '../utils/paginated-routes'; +import { SearchRoutesInput } from '../dtos/search-routes'; @Resolver(() => SearchResults) @UseInterceptors(DataLoaderInterceptor) @@ -24,4 +28,24 @@ export class SearchResolver { ): Promise { return this.searchService.find(input, user, gqlInfo); } + + @Query(() => PaginatedCrags) + @AllowAny() + @UseGuards(UserAuthGuard) + searchCrags( + @CurrentUser() user: User, + @Args('input') input?: SearchCragsInput, + ): Promise { + return this.searchService.paginatedCrags(input, user); + } + + @Query(() => PaginatedRoutes) + @AllowAny() + @UseGuards(UserAuthGuard) + searchRoutes( + @CurrentUser() user: User, + @Args('input') input?: SearchRoutesInput, + ): Promise { + return this.searchService.paginatedRoutes(input, user); + } } diff --git a/src/crags/services/search.service.ts b/src/crags/services/search.service.ts index 9c19531..3a29f68 100644 --- a/src/crags/services/search.service.ts +++ b/src/crags/services/search.service.ts @@ -8,6 +8,11 @@ import { Comment } from '../entities/comment.entity'; import { User } from '../../users/entities/user.entity'; import { SearchResults } from '../utils/search-results.class'; import { FieldNode, GraphQLResolveInfo } from 'graphql'; +import { SearchCragsInput } from '../dtos/search-crags.input'; +import { PaginatedCrags } from '../utils/paginated-crags'; +import { PaginationMeta } from '../../core/utils/pagination-meta.class'; +import { SearchRoutesInput } from '../dtos/search-routes'; +import { PaginatedRoutes } from '../utils/paginated-routes'; @Injectable() export class SearchService { @@ -65,7 +70,47 @@ export class SearchService { return result; } + async paginatedCrags( + params: SearchCragsInput, + currentUser: User, + ): Promise { + const query = this.buildFindCragsQuery(params.query, currentUser != null); + const itemCount = await query.getCount(); + + const pagination = new PaginationMeta( + itemCount, + params.pageNumber, + params.pageSize, + ); + + if (params.orderBy != null) { + if (params.orderBy.field == 'popularity') { + query + .addSelect('count(c.id)', 'nrvisits') + .leftJoin('activity', 'ac', 'ac.crag_id = c.id') + .groupBy('c.id') + .orderBy('nrvisits', 'DESC'); + } + } + + query + .skip(pagination.pageSize * (pagination.pageNumber - 1)) + .take(pagination.pageSize); + + return Promise.resolve({ + items: await query.getMany(), + meta: pagination, + }); + } + findCrags(searchString: string, showHidden: boolean): Promise { + return this.buildFindCragsQuery(searchString, showHidden).getMany(); + } + + buildFindCragsQuery( + searchString: string, + showHidden: boolean, + ): SelectQueryBuilder { const builder = this.cragsRepository.createQueryBuilder('c'); if (!showHidden) { @@ -76,10 +121,54 @@ export class SearchService { this.tokenizeQueryToBuilder(builder, searchString, 'c'); - return builder.getMany(); + return builder; + } + + async paginatedRoutes( + params: SearchRoutesInput, + currentUser: User, + ): Promise { + const query = this.buildFindRoutesQuery(params.query, currentUser != null); + const itemCount = await query.getCount(); + + const pagination = new PaginationMeta( + itemCount, + params.pageNumber, + params.pageSize, + ); + + if (params.orderBy != null) { + if (params.orderBy.field == 'popularity') { + query + .addSelect('count(r.id)', 'nrtries') + .leftJoin('activity_route', 'ar', 'ar.route_id = r.id') + .groupBy('r.id') + .orderBy('nrtries', 'DESC'); + } + } + + if (params.cragId) { + query.andWhere('r.crag_id = :cragId', { cragId: params.cragId }); + } + + query + .skip(pagination.pageSize * (pagination.pageNumber - 1)) + .take(pagination.pageSize); + + return Promise.resolve({ + items: await query.getMany(), + meta: pagination, + }); } findRoutes(searchString: string, showHidden: boolean): Promise { + return this.buildFindRoutesQuery(searchString, showHidden).getMany(); + } + + buildFindRoutesQuery( + searchString: string, + showHidden: boolean, + ): SelectQueryBuilder { const builder = this.routesRepository.createQueryBuilder('r'); builder.innerJoin('crag', 'c', 'c.id = r.crag_id'); @@ -92,7 +181,7 @@ export class SearchService { this.tokenizeQueryToBuilder(builder, searchString, 'r'); - return builder.getMany(); + return builder; } findSectors(searchString: string, showHidden: boolean): Promise { diff --git a/src/crags/utils/paginated-crags.ts b/src/crags/utils/paginated-crags.ts new file mode 100644 index 0000000..4db0c11 --- /dev/null +++ b/src/crags/utils/paginated-crags.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { Pagination } from '../../core/interfaces/pagination.interface'; +import { PaginationMeta } from '../../core/utils/pagination-meta.class'; +import { Crag } from '../entities/crag.entity'; + +@ObjectType() +export class PaginatedCrags implements Pagination { + @Field(() => [Crag]) + items: Crag[]; + @Field() + meta: PaginationMeta; +} diff --git a/src/crags/utils/paginated-routes.ts b/src/crags/utils/paginated-routes.ts new file mode 100644 index 0000000..f822d51 --- /dev/null +++ b/src/crags/utils/paginated-routes.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { Pagination } from '../../core/interfaces/pagination.interface'; +import { PaginationMeta } from '../../core/utils/pagination-meta.class'; +import { Route } from '../entities/route.entity'; + +@ObjectType() +export class PaginatedRoutes implements Pagination { + @Field(() => [Route]) + items: Route[]; + @Field() + meta: PaginationMeta; +} diff --git a/src/migration/1736070773650-addCustomActivityType.ts b/src/migration/1736070773650-addCustomActivityType.ts new file mode 100644 index 0000000..f583812 --- /dev/null +++ b/src/migration/1736070773650-addCustomActivityType.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class addCustomActivityType1736070773650 implements MigrationInterface { + name = 'addCustomActivityType1736070773650' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."activity_route_route_id_index"`); + await queryRunner.query(`ALTER TABLE "activity" ADD "custom_type" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "custom_type"`); + await queryRunner.query(`CREATE INDEX "activity_route_route_id_index" ON "activity_route" ("route_id") `); + } + +}