Skip to content

Commit

Permalink
[SHR-24] feat: search campaigns in explore screen (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
phamhongphuc1403 authored Mar 30, 2024
1 parent b77cbaf commit c53378f
Show file tree
Hide file tree
Showing 19 changed files with 215 additions and 29 deletions.
4 changes: 2 additions & 2 deletions backend/src/core/api/v1/campaign/campaign.resolver.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Module } from 'packages/handler/Module';
import { SearchCampaignInterceptor, CreateCampaignInterceptor } from 'core/modules/campaign/interceptor';
import { CampaignController } from './campaign.controller';
import { orgCampaignId, campaignId, RecordId, NameQuery, LongitudeQuery, LatitudeQuery, userId, WardQuery, DistrictQuery, CityQuery, CampaignSortQuery } from '../../../common/swagger';
import { orgCampaignId, campaignId, RecordId, NameQuery, LongitudeQuery, LatitudeQuery, userId, WardQuery, DistrictQuery, CityQuery, CampaignOrderQuery, CurrentLatitudeQuery, CurrentLongitudeQuery } from '../../../common/swagger';
import { RecordIdInterceptor } from '../../../modules/interceptor/recordId/record-id.interceptor';
import { FeedbackInterceptor } from '../../../modules/feedback';
import { MediaInterceptor } from 'core/modules/document';
Expand Down Expand Up @@ -62,7 +62,7 @@ export const CampaignResolver = Module.builder()
{
route: '/campaigns',
method: 'get',
params: [NameQuery, LongitudeQuery, LatitudeQuery, WardQuery, DistrictQuery, CityQuery, CampaignSortQuery],
params: [NameQuery, LatitudeQuery, LongitudeQuery, WardQuery, DistrictQuery, CityQuery, CampaignOrderQuery, CurrentLatitudeQuery, CurrentLongitudeQuery],
interceptors: [SearchCampaignInterceptor],
controller: CampaignController.searchByQuery,
},
Expand Down
17 changes: 17 additions & 0 deletions backend/src/core/common/orders/and-then-order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Order } from './'

export class ThenByOrder extends Order {
#firstOrder;
#secondOrder;

constructor(firstOrder, secondOrder) {
super();
this.#firstOrder = firstOrder;
this.#secondOrder = secondOrder;
}

toSql() {
console.log(123,this.#firstOrder.toSql(), " ",this.#secondOrder.toSql());
return `${this.#firstOrder.toSql()}, ${this.#secondOrder.toSql()}`;
}
}
2 changes: 2 additions & 0 deletions backend/src/core/common/orders/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './order';
export * from './and-then-order';
22 changes: 22 additions & 0 deletions backend/src/core/common/orders/order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ThenByOrder } from './';

export class Order {
constructor(direction = null) {
if (direction) {
this.validateDirection(direction);
}
this.direction = direction;
}

validateDirection(direction) {
var direction = direction.toUpperCase();

if (direction !== 'ASC' && direction !== 'DESC') {
throw new Error('Invalid direction');
}
}

thenBy(order) {
return new ThenByOrder(this, order);
}
}
9 changes: 5 additions & 4 deletions backend/src/core/common/swagger/campaign-sort-query.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { OrderFactory } from 'core/modules/campaign/orders';
import { SwaggerDocument } from '../../../packages/swagger';

export const CampaignSortQuery = SwaggerDocument.ApiParams({
name: 'isSortedByStatus',
export const CampaignOrderQuery = SwaggerDocument.ApiParams({
name: 'orderBy',
paramsIn: 'query',
type: 'bool',
type: 'string',
required: false,
description: 'Sort campaigns by status as follows: UPCOMING, ENDED, IN PROGRESS',
description: `Syntax follows the pattern 'value + 'asc' or 'desc' (+ ', ') (e.g., 'value1 asc, value2 desc'). Allowed values include: ${OrderFactory.getAllOrderQueryValues()}.`,
});
11 changes: 11 additions & 0 deletions backend/src/core/common/swagger/current-latitude-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SwaggerDocument } from '../../../packages/swagger';

export const CurrentLatitudeQuery = SwaggerDocument.ApiParams(
{
name: 'currentLat',
paramsIn: 'query',
type: 'string',
required: false,
description: "User's current latitude",
}
);
11 changes: 11 additions & 0 deletions backend/src/core/common/swagger/current-longitude-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SwaggerDocument } from '../../../packages/swagger';

export const CurrentLongitudeQuery = SwaggerDocument.ApiParams(
{
name: 'currentLng',
paramsIn: 'query',
type: 'string',
required: false,
description: "User's current longitude",
}
);
2 changes: 2 additions & 0 deletions backend/src/core/common/swagger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export * from './ward-query';
export * from './district-query';
export * from './city-query';
export * from './campaign-sort-query';
export * from './current-latitude-query';
export * from './current-longitude-query';
20 changes: 7 additions & 13 deletions backend/src/core/modules/campaign/campaign.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,15 +331,15 @@ class Repository extends DataRepository {
return super.removeNameConstraint();
}

findByQuery(specification, isSortedByStatus = false) {
var sql = specification.toSql();
var query = sql[0];
var params = sql[1];
findByQuery(specification, order) {
var whereSql = specification.toSql();
var whereQuery = whereSql[0];
var whereParams = whereSql[1];

var query = this.query()
.join('organizations', 'campaigns.organization_id', '=', 'organizations.id')
.whereNull('campaigns.deleted_at')
.whereRaw(query, params)
.whereRaw(whereQuery, whereParams)
.select([
'campaigns.id',
'campaigns.name',
Expand All @@ -360,14 +360,8 @@ class Repository extends DataRepository {
)`)}
]);

if (isSortedByStatus === 'true') {
query = query.orderByRaw(`
CASE
WHEN campaigns.start_date > NOW() AND campaigns.end_date > NOW() THEN 1 -- UPCOMING
WHEN campaigns.start_date < NOW() AND campaigns.end_date < NOW() THEN 2 -- PASSED
ELSE 2 -- IN PROGRESS
END
`);
if (order) {
query = query.orderByRaw(order.toSql());
}

return query;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Joi from 'joi';
import { DefaultValidatorInterceptor } from 'core/infrastructure/interceptor';
import { JoiUtils } from '../../../utils';

export const SearchCampaignInterceptor = new DefaultValidatorInterceptor(
Joi.object({
Expand All @@ -9,6 +10,8 @@ export const SearchCampaignInterceptor = new DefaultValidatorInterceptor(
ward: Joi.string().optional(),
district: Joi.string().optional(),
city: Joi.string().optional(),
isSortedByStatus: Joi.boolean().optional(),
orderBy: JoiUtils.order().optional(),
currentLat: Joi.number().optional(),
currentLng: Joi.number().optional(),
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Order } from 'core/common/orders';

export class CampaignByProximityOrder extends Order {
#lat;
#lng;

constructor(lat, lng, direction = 'ASC') {
super(direction);
if (!lat || !lng) throw new Error('Latitude and longitude are required');
this.#lat = parseFloat(lat);
this.#lng = parseFloat(lng);
}

toSql() {
return `ST_Distance(
ST_MakePoint(cast(coordinate->>'lng' as float), cast(coordinate->>'lat' as float))::geography,
ST_MakePoint(${this.#lng}, ${this.#lat})::geography) ${this.direction}`;
}
}
24 changes: 24 additions & 0 deletions backend/src/core/modules/campaign/orders/campaign-status-order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Order } from 'core/common/orders';

export class CampaignStatusOrder extends Order {
#upComing;
#onGoing;
#passed;

constructor(upComing, onGoing, passed, direction = 'ASC') {
super(direction);
this.#upComing = Number(upComing);
this.#onGoing = Number(onGoing);
this.#passed = Number(passed);
}

toSql() {
return `
CASE
WHEN campaigns.start_date > NOW() AND campaigns.end_date > NOW() THEN ${this.#upComing} -- UPCOMING
WHEN campaigns.start_date < NOW() AND campaigns.end_date < NOW() THEN ${this.#passed} -- PASSED
ELSE ${this.#onGoing} -- ONGOING
END ${this.direction}
`;
}
}
3 changes: 3 additions & 0 deletions backend/src/core/modules/campaign/orders/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './campaign-by-proximity-order';
export * from './campaign-status-order';
export * from './order-factory';
28 changes: 28 additions & 0 deletions backend/src/core/modules/campaign/orders/order-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CampaignByProximityOrder, CampaignStatusOrder } from '.'

export class OrderFactory {
static OrderTypeEnum = {
PROXIMITY: 'proximity',
PASSED_ONGOING_UPCOMING: 'passed_ongoing_upcoming',
UPCOMING_PASSED_ONGOING: 'upcoming_passed_ongoing',
PASSED_UPCOMING_ONGOING: 'passed_upcoming_ongoing',
};
static getOrder(param, lat, lng, direction) {
switch(param) {
case OrderTypeEnum.PROXIMITY:
return new CampaignByProximityOrder(lat, lng, direction);
case OrderTypeEnum.PASSED_ONGOING_UPCOMING:
return new CampaignStatusOrder(3, 2, 1, direction);
case OrderTypeEnum.UPCOMING_PASSED_ONGOING:
return new CampaignStatusOrder(1, 3, 2, direction);
case OrderTypeEnum.PASSED_UPCOMING_ONGOING:
return new CampaignStatusOrder(2, 3, 1, direction);
default:
throw new Error('Order not found');
}
}

static getAllOrderQueryValues() {
return Object.values(this.OrderTypeEnum).join(", ");
}
}
32 changes: 30 additions & 2 deletions backend/src/core/modules/campaign/services/campaign.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { UserCampaignRepository } from '../../user_campaign/user_campaign.reposi
import { Status } from '../../../common/enum';
import { UserRepository } from '../../../modules/user/user.repository';
import { FileSystemService, MediaService } from 'core/modules/document';
import { CampaignNamePartialMatchSpecification, CampaignWardSpecification, CampaignDistrictSpecification, CampaignCitySpecification, CampaignCoordinateSpecification } from '../specifications';
import { CampaignNamePartialMatchSpecification, CampaignWardSpecification, CampaignDistrictSpecification, CampaignCitySpecification, CampaignCoordinateSpecification, CampaignLocationProximitySpecification } from '../specifications';
import { TrueSpecification } from 'core/common/specifications';
import { OrderFactory } from '../orders';

class Service {
constructor() {
Expand Down Expand Up @@ -425,13 +426,40 @@ class Service {
specification = specification.and(campaignCoordinateSpecification);
}

var order = this.buildCampaignOrder(query.currentLat, query.currentLng, query.orderBy);

try {
return this.repository.findByQuery(specification, query.isSortedByStatus);
return this.repository.findByQuery(specification, order);
} catch (error) {
logger.error(error.message);
throw new InternalServerException();
}
}

buildCampaignOrder(lat, lng, orderBy) {
if (!orderBy) {
return null;
}

var orderStrategy = null;

orderBy.split(', ').forEach(value => {
console.log(value)
var array = value.split(' ');
var param = array[0];
var direction = array[1];

var order = OrderFactory.getOrder(param, lat, lng, direction);

if (orderStrategy) {
orderStrategy = orderStrategy.thenBy(order);
} else {
orderStrategy = order;
}
});
return orderStrategy;
}

};

export const CampaignService = new Service();
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Specification } from 'core/common/specifications';

export class CampaignLocationProximitySpecification extends Specification {
#lat;
#lng;
#radius = 5000;

constructor(lat, lng) {
super();
this.#lat = lat;
this.#lng = lng;
}

toSql() {
return [`ST_DWithin(
ST_MakePoint(cast(coordinate->>'lng' as float), cast(coordinate->>'lat' as float))::geography,
ST_MakePoint(:lng, :lat)::geography,:radius)`,
{ lng: this.#lng, lat: this.#lat, radius: this.#radius }];
}
}
1 change: 1 addition & 0 deletions backend/src/core/modules/campaign/specifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './campaign-ward-specification';
export * from './campaign-district-specification';
export * from './campaign-city-specification';
export * from './campaign-coordinate-specification';
export * from './campaign-location-proximity-specification'
7 changes: 7 additions & 0 deletions backend/src/core/utils/joi.util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const PWD_FORMAT = /^[a-zA-Z0-9\d@$!%*?&]{8,30}$/;

// Vietnam phone number validation
const PHONE_NUMBER_FORMAT = /(((\+|)84)|0)(3|5|7|8|9)+([0-9]{8})\b/;

const ORDER_FORMAT = /^(\w+\s(ASC|DESC|asc|desc))(,\s\w+\s(ASC|DESC|asc|desc))*$/;

export class JoiUtils {
static objectId() {
return Joi.string().regex(MONGOOSE_ID_OBJECT_FORMAT);
Expand Down Expand Up @@ -70,4 +73,8 @@ export class JoiUtils {
static phoneNumber() {
return Joi.string().regex(PHONE_NUMBER_FORMAT);
}

static order() {
return Joi.string().regex(ORDER_FORMAT);
}
}

0 comments on commit c53378f

Please sign in to comment.