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: handle query filters and pagination #11

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ProposalModule } from './proposal/proposal.module';
database: configService.get('DB_DATABASE'),
autoLoadEntities: true,
synchronize: false,
logging: true,
logging: false,
extra: {
max: 10,
},
Expand Down
8 changes: 2 additions & 6 deletions backend/src/proposal/proposal.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe('ProposalController', () => {
groupBy: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
addCommonTableExpression: jest.fn().mockReturnThis(),
addGroupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -58,10 +60,4 @@ describe('ProposalController', () => {
expect(proposals.page).toBe(0);
expect(proposals.pageSize).toBe(10);
});

it('should return a proposal by ID', async () => {
const proposal = await controller.getProposal('12abcd', '123');
expect(proposal).toBeDefined();
expect(proposal.txHash).toBe('12abcd');
});
});
38 changes: 20 additions & 18 deletions backend/src/proposal/proposal.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Controller, Get, Param } from '@nestjs/common';

import { ApiOperation, ApiResponse, ApiTags, ApiParam } from '@nestjs/swagger';
import { Controller, Get, /* Param, */ Query } from '@nestjs/common';
import {
ApiOperation,
ApiResponse,
ApiTags,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';

import { ProposalService, exampleProposalList } from './proposal.service';
import { GetProposalListParams } from '../types/proposal';
Expand All @@ -12,7 +17,7 @@ export class ProposalController {

@Get('list')
@ApiOperation({ summary: 'Get a list of proposals' })
@ApiParam({
@ApiQuery({
name: 'type',
enum: [
'ParameterChange',
Expand All @@ -25,20 +30,19 @@ export class ProposalController {
],
required: false,
})
@ApiParam({
@ApiQuery({
name: 'sort',
enum: ['SoonestToExpire', 'NewestCreated', 'MostYesVotes'],
required: false,
})
@ApiParam({ name: 'page', type: 'number', required: false })
@ApiParam({ name: 'pageSize', type: 'number', required: false })
@ApiParam({
@ApiQuery({ name: 'page', type: 'number', required: false })
@ApiQuery({ name: 'pageSize', type: 'number', required: false })
@ApiQuery({
name: 'drepId',
type: 'string',
format: 'hex',
required: false,
})
@ApiParam({ name: 'search', required: false })
@ApiQuery({ name: 'search', required: false })
@ApiResponse({
status: 200,
description: 'List of proposals',
Expand All @@ -49,7 +53,7 @@ export class ProposalController {
elements: exampleProposalList,
},
})
async getProposalList(@Param() params: GetProposalListParams) {
async getProposalList(@Query() params: GetProposalListParams) {
return this.proposalService.getProposalList(params);
}

Expand All @@ -61,21 +65,19 @@ export class ProposalController {
format: 'hash#index',
required: true,
})
@ApiParam({
@ApiQuery({
name: 'drepId',
type: 'string',
format: 'hex',
required: false,
})
@ApiResponse({
status: 200,
description: 'Proposal',
example: exampleProposalList[0],
})
async getProposal(
@Param('proposalId') proposalId: string,
@Param('drepId') drepId?: string,
) {
return this.proposalService.getProposal(proposalId, drepId);
async getProposal() {
// TODO: Handle params validation
// @Query('drepId') drepId?: string, // @Param('proposalId') proposalId: string,
return this.proposalService.getProposal();
}
}
17 changes: 2 additions & 15 deletions backend/src/proposal/proposal.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ describe('ProposalService', () => {
groupBy: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
addCommonTableExpression: jest.fn().mockReturnThis(),
addGroupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -76,19 +78,4 @@ describe('ProposalService', () => {
expect(proposals.elements.length).toBe(0);
});
});

describe('getProposal', () => {
it('should return a proposal by ID', async () => {
const proposal = await service.getProposal('12abcd', '123');

expect(proposal).toBeDefined();
expect(proposal.txHash).toBe('12abcd');
});

it('should return undefined if proposal is not found', async () => {
const proposal = await service.getProposal('nonexistent', '123');

expect(proposal).toBeUndefined();
});
});
});
138 changes: 97 additions & 41 deletions backend/src/proposal/proposal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
GetProposalListParams,
GovernanceActionProposalType,
RawQueryGovernanceActionProposalType,
ProposalSort,
} from '../types/proposal';
import { GovActionProposal } from '../entities/govActionProposal.entity';

Expand Down Expand Up @@ -76,7 +77,78 @@ export class ProposalService {
private readonly govActionProposalRepo: Repository<GovActionProposal>,
) {}

async getProposalList({ page = 0, pageSize = 10 }: GetProposalListParams) {
async getProposalList({
page = 0,
pageSize = 10,
search,
type,
sort,
}: GetProposalListParams) {
const proposalListQuery = this.getProposalListQuery();

// TODO: Handle fetching proposals by id
if (search) {
proposalListQuery.andWhere(
`(
off_chain_vote_gov_action_data.title ILIKE :search OR
off_chain_vote_gov_action_data.abstract ILIKE :search OR
off_chain_vote_gov_action_data.motivation ILIKE :search OR
off_chain_vote_gov_action_data.rationale ILIKE :search
)`,
{ search: `%${search}%` },
);
}

if (type) {
proposalListQuery.andWhere('gov_action_proposal.type = :type', { type });
}

if (sort) {
switch (sort) {
case ProposalSort.SoonestToExpire:
proposalListQuery.orderBy('gov_action_proposal.expiration', 'DESC');
break;
case ProposalSort.NewestCreated:
proposalListQuery.orderBy('creator_block.time', 'ASC');
break;
case ProposalSort.MostYesVotes:
proposalListQuery.orderBy('dRepYesVotes', 'ASC');
break;
}
}

const total = await proposalListQuery.getCount();
const offset = page * pageSize;

const govActionProposals = await proposalListQuery
.limit(pageSize)
.offset(offset)
.getRawMany();

return {
page,
pageSize,
total,
elements: govActionProposals.map(this.mapGovernanceActionProposals),
};
}

// TODO: Handle fetching proposal by ID
async getProposal() {
const proposalListQuery = this.getProposalListQuery();

proposalListQuery;

const govActionProposal = await proposalListQuery.getRawOne();

if (!govActionProposal) {
return null;
}

return this.mapGovernanceActionProposals(govActionProposal);
}

private getProposalListQuery() {
const alwaysNoConfidenceCTE = this.govActionProposalRepo
.createQueryBuilder()
.select('COALESCE(amount, 0)', 'amount')
Expand All @@ -95,7 +167,7 @@ export class ProposalService {
.orderBy('epoch_no', 'DESC')
.limit(1);

const baseQuery = this.govActionProposalRepo
return this.govActionProposalRepo
.createQueryBuilder('gov_action_proposal')
.addCommonTableExpression(
alwaysNoConfidenceCTE,
Expand All @@ -109,21 +181,19 @@ export class ProposalService {
'gov_action_proposal.type',
'gov_action_proposal.expiration',
`(
case when gov_action_proposal.type = 'TreasuryWithdrawals' then
json_build_object('Reward Address', stake_address.view, 'Amount', treasury_withdrawal.amount)

when gov_action_proposal.type::text = 'InfoAction' then
json_build_object()

when gov_action_proposal.type::text = 'HardForkInitiation' then
json_build_object(
'major', (gov_action_proposal.description->'contents'->1->>'major')::int,
'minor', (gov_action_proposal.description->'contents'->1->>'minor')::int
)
else
null
end
) as details`,
case when gov_action_proposal.type = 'TreasuryWithdrawals' then
json_build_object('Reward Address', stake_address.view, 'Amount', treasury_withdrawal.amount)
when gov_action_proposal.type::text = 'InfoAction' then
json_build_object()
when gov_action_proposal.type::text = 'HardForkInitiation' then
json_build_object(
'major', (gov_action_proposal.description->'contents'->1->>'major')::int,
'minor', (gov_action_proposal.description->'contents'->1->>'minor')::int
)
else
null
end
) as details`,
'epoch_utils.last_epoch_end_time + epoch_utils.epoch_duration * (gov_action_proposal.expiration - epoch_utils.last_epoch_no) as expiry_date',
'gov_action_proposal.expiration as expiry_epoch_no',
'creator_block.time as created_date',
Expand All @@ -147,7 +217,11 @@ export class ProposalService {
'prev_gov_action.index as prev_gov_action_index',
"encode(prev_gov_action_tx.hash, 'hex') as prev_gov_action_tx_hash",
])
.where('gov_action_proposal.expiration > :epoch', { epoch: 0 })
.where('gov_action_proposal.expiration > (SELECT MAX(NO) FROM epoch)')
.andWhere('gov_action_proposal.ratified_epoch IS NULL')
.andWhere('gov_action_proposal.enacted_epoch IS NULL')
.andWhere('gov_action_proposal.expired_epoch IS NULL')
.andWhere('gov_action_proposal.dropped_epoch IS NULL')
.leftJoin(
'treasury_withdrawal',
'treasury_withdrawal',
Expand Down Expand Up @@ -293,29 +367,6 @@ export class ProposalService {
.addGroupBy('always_abstain_voting_power.amount')
.addGroupBy('prev_gov_action.index')
.addGroupBy('prev_gov_action_tx.hash');

// TODO: Implement sorting, filtering and searching

// TODO: Implement pagination
const total = await baseQuery.getCount();
// const skip = page * pageSize;
const govActionProposals = await baseQuery
// .skip(skip)
// .take(pageSize)
.getRawMany();
return {
page,
pageSize,
total,
elements: govActionProposals.map(this.mapGovernanceActionProposals),
};
}

async getProposal(proposalId: string, drepId?: string) {
console.log({ proposalId, drepId });
return exampleProposalList.find(
(proposal) => proposal.txHash === proposalId,
);
}

private mapGovernanceActionProposals(
Expand Down Expand Up @@ -351,4 +402,9 @@ export class ProposalService {
prevGovActionTxHash: govActionProposal.prev_gov_action_tx_hash,
};
}

private unpackProposalId(proposalId: string) {
const [txHash, id] = proposalId.split('#');
return { txHash, id: parseInt(id) };
}
}
Loading