diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 466389af92..39a49cfbb9 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -92,6 +92,7 @@ export type AssetFeesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type AssetEdge = { @@ -893,6 +894,7 @@ export type QueryAssetsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -912,6 +914,7 @@ export type QueryPaymentsArgs = { filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -925,6 +928,7 @@ export type QueryPeersArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -943,6 +947,7 @@ export type QueryWalletAddressesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -952,6 +957,7 @@ export type QueryWebhookEventsArgs = { filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type Quote = { @@ -1056,6 +1062,13 @@ export type SetFeeResponse = MutationResponse & { success: Scalars['Boolean']['output']; }; +export enum SortOrder { + /** Choose ascending order for results. */ + Asc = 'ASC', + /** Choose descending order for results. */ + Desc = 'DESC' +} + export type TransferMutationResponse = MutationResponse & { __typename?: 'TransferMutationResponse'; code: Scalars['String']['output']; @@ -1169,6 +1182,7 @@ export type WalletAddressIncomingPaymentsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -1177,6 +1191,7 @@ export type WalletAddressOutgoingPaymentsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -1185,6 +1200,7 @@ export type WalletAddressQuotesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type WalletAddressEdge = { @@ -1436,6 +1452,7 @@ export type ResolversTypes = { RevokeWalletAddressKeyMutationResponse: ResolverTypeWrapper>; SetFeeInput: ResolverTypeWrapper>; SetFeeResponse: ResolverTypeWrapper>; + SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; TransferMutationResponse: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; diff --git a/packages/backend/src/asset/service.test.ts b/packages/backend/src/asset/service.test.ts index f5f7452ea7..ef0943c343 100644 --- a/packages/backend/src/asset/service.test.ts +++ b/packages/backend/src/asset/service.test.ts @@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid' import { AssetError, isAssetError } from './errors' import { AssetService } from './service' -import { Pagination } from '../shared/baseModel' +import { Pagination, SortOrder } from '../shared/baseModel' import { getPageTests } from '../shared/baseModel.test' import { createTestApp, TestContainer } from '../tests/app' import { createAsset, randomAsset } from '../tests/asset' @@ -210,7 +210,8 @@ describe('Asset Service', (): void => { describe('getPage', (): void => { getPageTests({ createModel: () => createAsset(deps), - getPage: (pagination?: Pagination) => assetService.getPage(pagination) + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + assetService.getPage(pagination, sortOrder) }) }) diff --git a/packages/backend/src/asset/service.ts b/packages/backend/src/asset/service.ts index f2ce5a0361..442319cf8f 100644 --- a/packages/backend/src/asset/service.ts +++ b/packages/backend/src/asset/service.ts @@ -2,7 +2,7 @@ import { NotFoundError, UniqueViolationError } from 'objection' import { AssetError } from './errors' import { Asset } from './model' -import { Pagination } from '../shared/baseModel' +import { Pagination, SortOrder } from '../shared/baseModel' import { BaseService } from '../shared/baseService' import { AccountingService, LiquidityAccountType } from '../accounting/service' @@ -26,7 +26,7 @@ export interface AssetService { create(options: CreateOptions): Promise update(options: UpdateOptions): Promise get(id: string): Promise - getPage(pagination?: Pagination): Promise + getPage(pagination?: Pagination, sortOrder?: SortOrder): Promise getAll(): Promise } @@ -51,7 +51,8 @@ export async function createAssetService({ create: (options) => createAsset(deps, options), update: (options) => updateAsset(deps, options), get: (id) => getAsset(deps, id), - getPage: (pagination?) => getAssetsPage(deps, pagination), + getPage: (pagination?, sortOrder?) => + getAssetsPage(deps, pagination, sortOrder), getAll: () => getAll(deps) } } @@ -119,9 +120,10 @@ async function getAsset( async function getAssetsPage( deps: ServiceDependencies, - pagination?: Pagination + pagination?: Pagination, + sortOrder?: SortOrder ): Promise { - return await Asset.query(deps.knex).getPage(pagination) + return await Asset.query(deps.knex).getPage(pagination, sortOrder) } async function getAll(deps: ServiceDependencies): Promise { diff --git a/packages/backend/src/fee/service.test.ts b/packages/backend/src/fee/service.test.ts index 2145321b57..e57068f7e3 100644 --- a/packages/backend/src/fee/service.test.ts +++ b/packages/backend/src/fee/service.test.ts @@ -13,7 +13,7 @@ import { v4 } from 'uuid' import { FeeError } from './errors' import { getPageTests } from '../shared/baseModel.test' import { createFee } from '../tests/fee' -import { Pagination } from '../shared/baseModel' +import { Pagination, SortOrder } from '../shared/baseModel' describe('Fee Service', (): void => { let deps: IocContract @@ -160,8 +160,8 @@ describe('Fee Service', (): void => { describe('Fee pagination', (): void => { getPageTests({ createModel: () => createFee(deps, asset.id), - getPage: (pagination?: Pagination) => - feeService.getPage(asset.id, pagination) + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + feeService.getPage(asset.id, pagination, sortOrder) }) }) }) diff --git a/packages/backend/src/fee/service.ts b/packages/backend/src/fee/service.ts index 89b147817b..4979cb18a4 100644 --- a/packages/backend/src/fee/service.ts +++ b/packages/backend/src/fee/service.ts @@ -2,7 +2,7 @@ import { ForeignKeyViolationError } from 'objection' import { BaseService } from '../shared/baseService' import { FeeError } from './errors' import { Fee, FeeType } from './model' -import { Pagination } from '../shared/baseModel' +import { Pagination, SortOrder } from '../shared/baseModel' export interface CreateOptions { assetId: string @@ -15,7 +15,11 @@ export interface CreateOptions { export interface FeeService { create(CreateOptions: CreateOptions): Promise - getPage(assetId: string, pagination?: Pagination): Promise + getPage( + assetId: string, + pagination?: Pagination, + sortOrder?: SortOrder + ): Promise getLatestFee(assetId: string, type: FeeType): Promise } @@ -33,8 +37,11 @@ export async function createFeeService({ } return { create: (options: CreateOptions) => createFee(deps, options), - getPage: (assetId: string, pagination: Pagination) => - getFeesPage(deps, assetId, pagination), + getPage: ( + assetId: string, + pagination: Pagination, + sortOrder = SortOrder.Desc + ) => getFeesPage(deps, assetId, pagination, sortOrder), getLatestFee: (assetId: string, type: FeeType) => getLatestFee(deps, assetId, type) } @@ -43,9 +50,12 @@ export async function createFeeService({ async function getFeesPage( deps: ServiceDependencies, assetId: string, - pagination?: Pagination + pagination?: Pagination, + sortOrder?: SortOrder ): Promise { - const query = Fee.query(deps.knex).where({ assetId }).getPage(pagination) + const query = Fee.query(deps.knex) + .where({ assetId }) + .getPage(pagination, sortOrder) return await query } diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 8a90990062..aa3fa5ec2f 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -381,6 +381,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -5571,6 +5583,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -5706,6 +5730,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -5800,6 +5836,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -5923,6 +5971,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -6000,6 +6060,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -6775,6 +6847,29 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "SortOrder", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ASC", + "description": "Choose ascending order for results.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DESC", + "description": "Choose descending order for results.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "SCALAR", "name": "String", @@ -7512,6 +7607,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -7573,6 +7680,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -7646,6 +7765,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "sortOrder", + "description": "Ascending or descending order of creation.", + "type": { + "kind": "ENUM", + "name": "SortOrder", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 466389af92..39a49cfbb9 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -92,6 +92,7 @@ export type AssetFeesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type AssetEdge = { @@ -893,6 +894,7 @@ export type QueryAssetsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -912,6 +914,7 @@ export type QueryPaymentsArgs = { filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -925,6 +928,7 @@ export type QueryPeersArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -943,6 +947,7 @@ export type QueryWalletAddressesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -952,6 +957,7 @@ export type QueryWebhookEventsArgs = { filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type Quote = { @@ -1056,6 +1062,13 @@ export type SetFeeResponse = MutationResponse & { success: Scalars['Boolean']['output']; }; +export enum SortOrder { + /** Choose ascending order for results. */ + Asc = 'ASC', + /** Choose descending order for results. */ + Desc = 'DESC' +} + export type TransferMutationResponse = MutationResponse & { __typename?: 'TransferMutationResponse'; code: Scalars['String']['output']; @@ -1169,6 +1182,7 @@ export type WalletAddressIncomingPaymentsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -1177,6 +1191,7 @@ export type WalletAddressOutgoingPaymentsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -1185,6 +1200,7 @@ export type WalletAddressQuotesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type WalletAddressEdge = { @@ -1436,6 +1452,7 @@ export type ResolversTypes = { RevokeWalletAddressKeyMutationResponse: ResolverTypeWrapper>; SetFeeInput: ResolverTypeWrapper>; SetFeeResponse: ResolverTypeWrapper>; + SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; TransferMutationResponse: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; diff --git a/packages/backend/src/graphql/resolvers/asset.test.ts b/packages/backend/src/graphql/resolvers/asset.test.ts index c426f97ef3..5aa2ac4c54 100644 --- a/packages/backend/src/graphql/resolvers/asset.test.ts +++ b/packages/backend/src/graphql/resolvers/asset.test.ts @@ -14,6 +14,7 @@ import { isAssetError } from '../../asset/errors' import { Asset as AssetModel } from '../../asset/model' import { AssetService } from '../../asset/service' import { randomAsset } from '../../tests/asset' +import { SortOrder } from '../../shared/baseModel' import { AssetMutationResponse, Asset, @@ -396,6 +397,7 @@ describe('Asset Resolvers', (): void => { assert.ok(!isAssetError(asset)) assets.push(asset) } + assets.reverse() // Calling the default getPage will result in descending order const query = await appContainer.apolloClient .query({ query: gql` @@ -457,11 +459,15 @@ describe('Asset Resolvers', (): void => { test('Can get fees', async (): Promise => { const fees: Fee[] = [] + const sortOrder = SortOrder.Desc for (let i = 0; i < 2; i++) { const fee = await createFee(deps, assetId) assert.ok(!isFeeError(fee)) fees.push(fee) } + if (sortOrder === SortOrder.Desc) { + fees.reverse() + } const query = await appContainer.apolloClient .query({ query: gql` diff --git a/packages/backend/src/graphql/resolvers/asset.ts b/packages/backend/src/graphql/resolvers/asset.ts index df6f71f294..a12a14b11b 100644 --- a/packages/backend/src/graphql/resolvers/asset.ts +++ b/packages/backend/src/graphql/resolvers/asset.ts @@ -9,7 +9,7 @@ import { Asset } from '../../asset/model' import { AssetError, isAssetError } from '../../asset/errors' import { ApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' import { feeToGraphql } from './fee' import { Fee, FeeType } from '../../fee/model' @@ -19,10 +19,14 @@ export const getAssets: QueryResolvers['assets'] = async ( ctx ): Promise => { const assetService = await ctx.container.use('assetService') - const assets = await assetService.getPage(args) + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const assets = await assetService.getPage(pagination, order) const pageInfo = await getPageInfo( - (pagination: Pagination) => assetService.getPage(pagination), - assets + (pagination: Pagination, sortOrder?: SortOrder) => + assetService.getPage(pagination, sortOrder), + assets, + order ) return { pageInfo, @@ -165,16 +169,19 @@ export const getFees: AssetResolvers['fees'] = async ( args, ctx ): Promise => { - const { ...pagination } = args + const { sortOrder, ...pagination } = args const feeService = await ctx.container.use('feeService') - const getPageFn = (pagination_: Pagination) => { + const getPageFn = (pagination_: Pagination, sortOrder_?: SortOrder) => { if (!parent.id) throw new Error('missing asset id') - return feeService.getPage(parent.id, pagination_) + return feeService.getPage(parent.id, pagination_, sortOrder_) } - const fees = await getPageFn(pagination) + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const fees = await getPageFn(pagination, order) const pageInfo = await getPageInfo( - (pagination_: Pagination) => getPageFn(pagination_), - fees + (pagination_: Pagination, sortOrder_?: SortOrder) => + getPageFn(pagination_, sortOrder_), + fees, + order ) return { pageInfo, diff --git a/packages/backend/src/graphql/resolvers/combined_payments.test.ts b/packages/backend/src/graphql/resolvers/combined_payments.test.ts index f2699ac423..55581e5d65 100644 --- a/packages/backend/src/graphql/resolvers/combined_payments.test.ts +++ b/packages/backend/src/graphql/resolvers/combined_payments.test.ts @@ -112,7 +112,7 @@ describe('Payment', (): void => { PaymentType.Outgoing, outgoingPayment ) - expect(query.edges[0].node).toMatchObject({ + expect(query.edges[1].node).toMatchObject({ id: combinedOutgoingPayment.id, type: combinedOutgoingPayment.type, metadata: combinedOutgoingPayment.metadata, @@ -125,7 +125,7 @@ describe('Payment', (): void => { PaymentType.Incoming, incomingPayment ) - expect(query.edges[1].node).toMatchObject({ + expect(query.edges[0].node).toMatchObject({ id: combinedIncomingPayment.id, type: combinedIncomingPayment.type, metadata: combinedIncomingPayment.metadata, diff --git a/packages/backend/src/graphql/resolvers/combined_payments.ts b/packages/backend/src/graphql/resolvers/combined_payments.ts index b01c9cd9fc..45fad40732 100644 --- a/packages/backend/src/graphql/resolvers/combined_payments.ts +++ b/packages/backend/src/graphql/resolvers/combined_payments.ts @@ -5,7 +5,7 @@ import { } from '../generated/graphql' import { ApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' import { CombinedPayment } from '../../open_payments/payment/combined/model' export const getCombinedPayments: QueryResolvers['payments'] = @@ -13,15 +13,22 @@ export const getCombinedPayments: QueryResolvers['payments'] = const combinedPaymentService = await ctx.container.use( 'combinedPaymentService' ) - const { filter, ...pagination } = args + const { filter, sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc - const getPageFn = (pagination_: Pagination) => - combinedPaymentService.getPage({ pagination: pagination_, filter }) + const getPageFn = (pagination_: Pagination, sortOrder_?: SortOrder) => + combinedPaymentService.getPage({ + pagination: pagination_, + filter, + sortOrder: sortOrder_ + }) - const payments = await getPageFn(pagination) + const payments = await getPageFn(pagination, order) const pageInfo = await getPageInfo( - (pagination_: Pagination) => getPageFn(pagination_), - payments + (pagination_: Pagination, sortOrder_?: SortOrder) => + getPageFn(pagination_, sortOrder_), + payments, + order ) return { diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.ts b/packages/backend/src/graphql/resolvers/incoming_payment.ts index 1510c0f202..7ea3f4b3d7 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.ts @@ -14,7 +14,7 @@ import { } from '../../open_payments/payment/incoming/errors' import { ApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' export const getIncomingPayment: QueryResolvers['incomingPayment'] = async (parent, args, ctx): Promise => { @@ -38,17 +38,22 @@ export const getWalletAddressIncomingPayments: WalletAddressResolvers + (pagination: Pagination, sortOrder?: SortOrder) => incomingPaymentService.getWalletAddressPage({ walletAddressId: parent.id as string, - pagination + pagination, + sortOrder }), - incomingPayments + incomingPayments, + order ) return { diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.ts index 0c972fb0d4..553aaa0ad6 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.ts @@ -15,7 +15,7 @@ import { import { OutgoingPayment } from '../../open_payments/payment/outgoing/model' import { ApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' export const getOutgoingPayment: QueryResolvers['outgoingPayment'] = async (parent, args, ctx): Promise => { @@ -70,17 +70,22 @@ export const getWalletAddressOutgoingPayments: WalletAddressResolvers + (pagination: Pagination, sortOrder?: SortOrder) => outgoingPaymentService.getWalletAddressPage({ walletAddressId: parent.id as string, - pagination + pagination, + sortOrder }), - outgoingPayments + outgoingPayments, + order ) return { pageInfo, diff --git a/packages/backend/src/graphql/resolvers/page.test.ts b/packages/backend/src/graphql/resolvers/page.test.ts index bc92dc0b8e..b6313627b2 100644 --- a/packages/backend/src/graphql/resolvers/page.test.ts +++ b/packages/backend/src/graphql/resolvers/page.test.ts @@ -6,7 +6,7 @@ import { import { gql } from '@apollo/client' import { Model, PageInfo } from '../generated/graphql' -import { BaseModel } from '../../shared/baseModel' +import { BaseModel, SortOrder } from '../../shared/baseModel' interface PageTestsOptions { getClient: () => ApolloClient @@ -73,6 +73,7 @@ export const getPageTests = ({ describe('Common query resolver pagination', (): void => { let apolloClient: ApolloClient + const sortOrder = Math.random() < 0.5 ? SortOrder.Asc : SortOrder.Desc beforeAll((): void => { apolloClient = getClient() @@ -83,11 +84,13 @@ export const getPageTests = ({ for (let i = 0; i < 50; i++) { models.push(await createModel()) } + return models } test('pageInfo is correct on default query without params', async (): Promise => { const models = await createModels() + models.reverse() // default is SortOrder.Desc const query = await apolloClient .query({ query: parent @@ -151,26 +154,30 @@ export const getPageTests = ({ test('pageInfo is correct on pagination from start', async (): Promise => { const models = await createModels() + if (sortOrder === SortOrder.Desc) { + models.reverse() + } const query = await apolloClient .query({ query: parent ? gql` - query Page($id: String!) { + query Page($id: String!, $sortOrder: SortOrder) { ${parent.query}(id: $id) { - ${pagedQuery}(first: 10) { + ${pagedQuery}(first: 10, sortOrder: $sortOrder) { ${queryFields} } } }` : gql` - query Page { - ${pagedQuery}(first: 10) { + query Page($sortOrder: SortOrder) { + ${pagedQuery}(first: 10, sortOrder: $sortOrder) { ${queryFields} } } `, variables: { - id: parent?.getId() + id: parent?.getId(), + sortOrder: sortOrder === SortOrder.Asc ? 'ASC' : 'DESC' } }) .then(toConnection) @@ -183,27 +190,31 @@ export const getPageTests = ({ test('pageInfo is correct on pagination from middle', async (): Promise => { const models = await createModels() + if (sortOrder === SortOrder.Desc) { + models.reverse() + } const query = await apolloClient .query({ query: parent ? gql` - query Page($id: String!, $after: String!) { + query Page($id: String!, $after: String!, $sortOrder: SortOrder) { ${parent.query}(id: $id) { - ${pagedQuery}(after: $after) { + ${pagedQuery}(after: $after, sortOrder: $sortOrder) { ${queryFields} } } }` : gql` - query Page($after: String!) { - ${pagedQuery}(after: $after) { + query Page($after: String!, $sortOrder: SortOrder) { + ${pagedQuery}(after: $after, sortOrder: $sortOrder) { ${queryFields} } } `, variables: { id: parent?.getId(), - after: models[19].id + after: models[19].id, + sortOrder: sortOrder === SortOrder.Asc ? 'ASC' : 'DESC' } }) .then(toConnection) @@ -216,27 +227,31 @@ export const getPageTests = ({ test('pageInfo is correct on pagination near end', async (): Promise => { const models = await createModels() + if (sortOrder === SortOrder.Desc) { + models.reverse() + } const query = await apolloClient .query({ query: parent ? gql` - query Page($id: String!, $after: String!) { + query Page($id: String!, $after: String!, $sortOrder: SortOrder) { ${parent.query}(id: $id) { - ${pagedQuery}(after: $after, first: 10) { + ${pagedQuery}(after: $after, first: 10, sortOrder: $sortOrder) { ${queryFields} } } }` : gql` - query Page($after: String!) { - ${pagedQuery}(after: $after, first: 10) { + query Page($after: String!, $sortOrder: SortOrder) { + ${pagedQuery}(after: $after, first: 10, sortOrder: $sortOrder) { ${queryFields} } } `, variables: { id: parent?.getId(), - after: models[44].id + after: models[44].id, + sortOrder: sortOrder === SortOrder.Asc ? 'ASC' : 'DESC' } }) .then(toConnection) diff --git a/packages/backend/src/graphql/resolvers/peer.test.ts b/packages/backend/src/graphql/resolvers/peer.test.ts index a4e6bf842b..a24997566b 100644 --- a/packages/backend/src/graphql/resolvers/peer.test.ts +++ b/packages/backend/src/graphql/resolvers/peer.test.ts @@ -366,6 +366,7 @@ describe('Peer Resolvers', (): void => { for (let i = 0; i < 2; i++) { peers.push(await createPeer(deps, randomPeer())) } + peers.reverse() // Calling the default getPage will result in descending order const query = await appContainer.apolloClient .query({ query: gql` diff --git a/packages/backend/src/graphql/resolvers/peer.ts b/packages/backend/src/graphql/resolvers/peer.ts index 4a00043313..839883d123 100644 --- a/packages/backend/src/graphql/resolvers/peer.ts +++ b/packages/backend/src/graphql/resolvers/peer.ts @@ -14,7 +14,7 @@ import { } from '../../payment-method/ilp/peer/errors' import { ApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' export const getPeers: QueryResolvers['peers'] = async ( parent, @@ -22,10 +22,14 @@ export const getPeers: QueryResolvers['peers'] = async ( ctx ): Promise => { const peerService = await ctx.container.use('peerService') - const peers = await peerService.getPage(args) + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const peers = await peerService.getPage(pagination, order) const pageInfo = await getPageInfo( - (pagination: Pagination) => peerService.getPage(pagination), - peers + (pagination: Pagination, sortOrder?: SortOrder) => + peerService.getPage(pagination, sortOrder), + peers, + order ) return { pageInfo, diff --git a/packages/backend/src/graphql/resolvers/quote.ts b/packages/backend/src/graphql/resolvers/quote.ts index 07e2e2cf59..311bf9cfdb 100644 --- a/packages/backend/src/graphql/resolvers/quote.ts +++ b/packages/backend/src/graphql/resolvers/quote.ts @@ -14,7 +14,7 @@ import { import { Quote } from '../../open_payments/quote/model' import { ApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' import { CreateQuoteOptions } from '../../open_payments/quote/service' export const getQuote: QueryResolvers['quote'] = async ( @@ -67,17 +67,22 @@ export const getWalletAddressQuotes: WalletAddressResolvers['quot async (parent, args, ctx): Promise => { if (!parent.id) throw new Error('missing wallet address id') const quoteService = await ctx.container.use('quoteService') + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc const quotes = await quoteService.getWalletAddressPage({ walletAddressId: parent.id, - pagination: args + pagination, + sortOrder: order }) const pageInfo = await getPageInfo( - (pagination: Pagination) => + (pagination: Pagination, sortOrder?: SortOrder) => quoteService.getWalletAddressPage({ walletAddressId: parent.id as string, - pagination + pagination, + sortOrder }), - quotes + quotes, + order ) return { pageInfo, diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index a07ab9bf08..947cdc3426 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -462,6 +462,7 @@ describe('Wallet Address Resolvers', (): void => { for (let i = 0; i < 2; i++) { walletAddresses.push(await createWalletAddress(deps)) } + walletAddresses.reverse() // Calling the default getPage will result in descending order const query = await appContainer.apolloClient .query({ query: gql` diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 2fcfdd4a9f..5d1cce7e9f 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -15,7 +15,7 @@ import { } from '../../open_payments/wallet_address/errors' import { WalletAddress } from '../../open_payments/wallet_address/model' import { getPageInfo } from '../../shared/pagination' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' export const getWalletAddresses: QueryResolvers['walletAddresses'] = async ( @@ -24,9 +24,15 @@ export const getWalletAddresses: QueryResolvers['walletAddresses' ctx ): Promise => { const walletAddressService = await ctx.container.use('walletAddressService') - const walletAddresses = await walletAddressService.getPage(args) + const { sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const walletAddresses = await walletAddressService.getPage( + pagination, + order + ) const pageInfo = await getPageInfo( - (pagination: Pagination) => walletAddressService.getPage(pagination), + (pagination: Pagination, sortOrder?: SortOrder) => + walletAddressService.getPage(pagination, sortOrder), walletAddresses ) return { diff --git a/packages/backend/src/graphql/resolvers/webhooks.test.ts b/packages/backend/src/graphql/resolvers/webhooks.test.ts index 5157392dfa..34f0056fac 100644 --- a/packages/backend/src/graphql/resolvers/webhooks.test.ts +++ b/packages/backend/src/graphql/resolvers/webhooks.test.ts @@ -52,10 +52,11 @@ describe('Webhook Events Query', (): void => { ) } } + webhookEvents.reverse() // Calling the default getPage will result in descending order const filter = { type: { - in: [webhookEventTypes[0], webhookEventTypes[1]] + in: [webhookEventTypes[1], webhookEventTypes[2]] } } diff --git a/packages/backend/src/graphql/resolvers/webhooks.ts b/packages/backend/src/graphql/resolvers/webhooks.ts index 6cbc165125..9c9882b6fd 100644 --- a/packages/backend/src/graphql/resolvers/webhooks.ts +++ b/packages/backend/src/graphql/resolvers/webhooks.ts @@ -6,7 +6,7 @@ import { } from '../generated/graphql' import { getPageInfo } from '../../shared/pagination' import { WebhookEvent } from '../../webhook/model' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' export const getWebhookEvents: QueryResolvers['webhookEvents'] = async ( @@ -14,14 +14,21 @@ export const getWebhookEvents: QueryResolvers['webhookEvents'] = args, ctx ): Promise => { - const { filter, ...pagination } = args + const { filter, sortOrder, ...pagination } = args + const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc const webhookService = await ctx.container.use('webhookService') - const getPageFn = (pagination_: Pagination) => - webhookService.getPage({ pagination: pagination_, filter }) - const webhookEvents = await getPageFn(pagination) + const getPageFn = (pagination_: Pagination, sortOrder_?: SortOrder) => + webhookService.getPage({ + pagination: pagination_, + filter, + sortOrder: sortOrder_ + }) + const webhookEvents = await getPageFn(pagination, order) const pageInfo = await getPageInfo( - (pagination_: Pagination) => getPageFn(pagination_), - webhookEvents + (pagination_: Pagination, sortOrder_?: SortOrder) => + getPageFn(pagination_, sortOrder_), + webhookEvents, + order ) return { pageInfo, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 2acdc54e17..717d0cc77d 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -12,6 +12,8 @@ type Query { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): AssetsConnection! "Fetch a peer" @@ -27,6 +29,8 @@ type Query { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): PeersConnection! "Fetch a wallet address" @@ -42,6 +46,8 @@ type Query { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): WalletAddressesConnection! "Fetch an Open Payments quote" @@ -63,6 +69,8 @@ type Query { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder "Filter webhook events based on specific criteria." filter: WebhookEventFilter ): WebhookEventsConnection! @@ -77,6 +85,8 @@ type Query { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder "Filter payment events based on specific criteria." filter: PaymentFilter ): PaymentConnection! @@ -452,11 +462,20 @@ type Asset implements Model { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): FeesConnection "Date-time of creation" createdAt: String! } +enum SortOrder { + "Choose ascending order for results." + ASC + "Choose descending order for results." + DESC +} + enum LiquidityError { AlreadyPosted AlreadyVoided @@ -541,6 +560,8 @@ type WalletAddress implements Model { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): IncomingPaymentConnection "List of quotes created at this wallet address" quotes( @@ -552,6 +573,8 @@ type WalletAddress implements Model { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): QuoteConnection "List of outgoing payments sent from this wallet address" outgoingPayments( @@ -563,6 +586,8 @@ type WalletAddress implements Model { first: Int "Paginating backwards: The last **n** elements from the page." last: Int + "Ascending or descending order of creation." + sortOrder: SortOrder ): OutgoingPaymentConnection "Date-time of creation" createdAt: String! diff --git a/packages/backend/src/open_payments/payment/combined/service.test.ts b/packages/backend/src/open_payments/payment/combined/service.test.ts index d4de0b4a6b..fddb5efd43 100644 --- a/packages/backend/src/open_payments/payment/combined/service.test.ts +++ b/packages/backend/src/open_payments/payment/combined/service.test.ts @@ -14,7 +14,7 @@ import { createWalletAddress } from '../../../tests/walletAddress' import { createIncomingPayment } from '../../../tests/incomingPayment' -import { Pagination } from '../../../shared/baseModel' +import { Pagination, SortOrder } from '../../../shared/baseModel' import { PaymentType } from './model' import { Asset } from '../../../asset/model' import { @@ -85,8 +85,8 @@ describe('Combined Payment Service', (): void => { describe('CombinedPayment Service', (): void => { getPageTests({ createModel: () => createCombinedPayment(deps), - getPage: (pagination?: Pagination) => - combinedPaymentService.getPage({ pagination }) + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + combinedPaymentService.getPage({ pagination, sortOrder }) }) test('should return empty array if no payments', async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/combined/service.ts b/packages/backend/src/open_payments/payment/combined/service.ts index 022b42864d..74a1ced361 100644 --- a/packages/backend/src/open_payments/payment/combined/service.ts +++ b/packages/backend/src/open_payments/payment/combined/service.ts @@ -1,6 +1,6 @@ import { TransactionOrKnex } from 'objection' import { BaseService } from '../../../shared/baseService' -import { Pagination } from '../../../shared/baseModel' +import { Pagination, SortOrder } from '../../../shared/baseModel' import { CombinedPayment } from './model' import { FilterString } from '../../../shared/filters' import { OutgoingPaymentService } from '../outgoing/service' @@ -13,6 +13,7 @@ interface CombinedPaymentFilter { interface GetPageOptions { pagination?: Pagination filter?: CombinedPaymentFilter + sortOrder?: SortOrder } export interface CombinedPaymentService { @@ -42,7 +43,7 @@ async function getCombinedPaymentsPage( deps: ServiceDependencies, options?: GetPageOptions ) { - const { filter, pagination } = options ?? {} + const { filter, pagination, sortOrder } = options ?? {} const query = CombinedPayment.query(deps.knex) @@ -54,5 +55,5 @@ async function getCombinedPaymentsPage( query.whereIn('type', filter.type.in) } - return await query.getPage(pagination) + return await query.getPage(pagination, sortOrder) } diff --git a/packages/backend/src/open_payments/payment/incoming/routes.test.ts b/packages/backend/src/open_payments/payment/incoming/routes.test.ts index f06d159e71..1abbc8223d 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.test.ts @@ -27,6 +27,7 @@ import { createWalletAddress } from '../../../tests/walletAddress' import { Asset } from '../../../asset/model' import { IncomingPaymentError, errorToCode, errorToMessage } from './errors' import { IncomingPaymentService } from './service' +import { SortOrder } from '../../../shared/baseModel' import { IncomingPaymentWithPaymentMethods as OpenPaymentsIncomingPaymentWithPaymentMethods } from '@interledger/open-payments' describe('Incoming Payment Routes', (): void => { @@ -124,7 +125,8 @@ describe('Incoming Payment Routes', (): void => { return response }, list: (ctx) => incomingPaymentRoutes.list(ctx), - urlPath: IncomingPayment.urlPath + urlPath: IncomingPayment.urlPath, + sortOrder: SortOrder.Desc }) test('returns 500 for unexpected error', async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts index d9694e8bb8..f30e16de55 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -22,6 +22,7 @@ import { setup as setupContext } from '../../wallet_address/model.test' import { createOutgoingPayment } from '../../../tests/outgoingPayment' +import { SortOrder } from '../../../shared/baseModel' import { createWalletAddress } from '../../../tests/walletAddress' describe('Outgoing Payment Routes', (): void => { @@ -120,7 +121,8 @@ describe('Outgoing Payment Routes', (): void => { } }, list: (ctx) => outgoingPaymentRoutes.list(ctx), - urlPath: OutgoingPayment.urlPath + urlPath: OutgoingPayment.urlPath, + sortOrder: SortOrder.Desc }) test('returns 500 for unexpected error', async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/model.test.ts b/packages/backend/src/open_payments/wallet_address/model.test.ts index 1fb16bd872..5020876136 100644 --- a/packages/backend/src/open_payments/wallet_address/model.test.ts +++ b/packages/backend/src/open_payments/wallet_address/model.test.ts @@ -18,6 +18,7 @@ import { AuthenticatedStatusContext } from '../../app' import { getPageTests } from '../../shared/baseModel.test' +import { SortOrder } from '../../shared/baseModel' import { createContext } from '../../tests/context' import { createWalletAddress } from '../../tests/walletAddress' import { truncateTables } from '../../tests/tableManager' @@ -68,6 +69,7 @@ interface BaseTestsOptions { createModel: (options: { client?: string }) => Promise testGet: (options: TestGetOptions, expectedMatch?: M) => void testList?: (options: ListOptions, expectedMatch?: M) => void + sortOrder?: SortOrder } const baseGetTests = ({ @@ -176,10 +178,11 @@ export const getTests = ({ walletAddressId = model.walletAddressId return model }, - getPage: (pagination) => + getPage: (pagination, sortOrder) => list({ walletAddressId, - pagination + pagination, + sortOrder }) }) } @@ -193,6 +196,7 @@ type RouteTestsOptions = Omit< getBody: (model: M, list?: boolean) => Record list?: (ctx: ListContext) => Promise urlPath: string + sortOrder?: SortOrder } export const getRouteTests = ({ @@ -201,7 +205,8 @@ export const getRouteTests = ({ get, getBody, list, - urlPath + urlPath, + sortOrder }: RouteTestsOptions): void => { const testList = async ( { walletAddressId, client }: ListOptions, @@ -266,7 +271,8 @@ export const getRouteTests = ({ } }, // tests walletAddressId / client filtering - testList: list && testList + testList: list && testList, + sortOrder: sortOrder }) if (list) { @@ -278,6 +284,9 @@ export const getRouteTests = ({ for (let i = 0; i < 3; i++) { models.push(await createModel({})) } + if (sortOrder === SortOrder.Desc) { + models.reverse() + } }) test.each` diff --git a/packages/backend/src/open_payments/wallet_address/model.ts b/packages/backend/src/open_payments/wallet_address/model.ts index 9892c4b470..e7bbd8f341 100644 --- a/packages/backend/src/open_payments/wallet_address/model.ts +++ b/packages/backend/src/open_payments/wallet_address/model.ts @@ -3,7 +3,7 @@ import { WalletAddress as OpenPaymentsWalletAddress } from '@interledger/open-pa import { LiquidityAccount, OnCreditOptions } from '../../accounting/service' import { ConnectorAccount } from '../../payment-method/ilp/connector/core/rafiki' import { Asset } from '../../asset/model' -import { BaseModel, Pagination } from '../../shared/baseModel' +import { BaseModel, Pagination, SortOrder } from '../../shared/baseModel' import { WebhookEvent } from '../../webhook/model' import { WalletAddressKey } from '../../open_payments/wallet_address/key/model' import { AmountJSON } from '../amount' @@ -142,6 +142,7 @@ export interface ListOptions { walletAddressId: string client?: string pagination?: Pagination + sortOrder?: SortOrder } class SubresourceQueryBuilder< @@ -166,11 +167,11 @@ class SubresourceQueryBuilder< } return this.findById(id) } - list({ walletAddressId, client, pagination }: ListOptions) { + list({ walletAddressId, client, pagination, sortOrder }: ListOptions) { if (client) { this.where({ client }) } - return this.getPage(pagination).where( + return this.getPage(pagination, sortOrder).where( `${this.modelClass().tableName}.walletAddressId`, walletAddressId ) diff --git a/packages/backend/src/open_payments/wallet_address/service.test.ts b/packages/backend/src/open_payments/wallet_address/service.test.ts index 711bc39741..16750a970b 100644 --- a/packages/backend/src/open_payments/wallet_address/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/service.test.ts @@ -22,7 +22,7 @@ import { faker } from '@faker-js/faker' import { createIncomingPayment } from '../../tests/incomingPayment' import { getPageInfo } from '../../shared/pagination' import { getPageTests } from '../../shared/baseModel.test' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' import { sleep } from '../../shared/utils' import { withConfigOverride } from '../../tests/helpers' @@ -405,19 +405,24 @@ describe('Open Payments Wallet Address Service', (): void => { hasNextPage, hasPreviousPage }): Promise => { + const sortOrder = Math.random() < 0.5 ? SortOrder.Asc : SortOrder.Desc const walletAddressIds: string[] = [] for (let i = 0; i < num; i++) { const walletAddress = await createWalletAddress(deps) walletAddressIds.push(walletAddress.id) } + if (sortOrder === SortOrder.Desc) { + walletAddressIds.reverse() + } if (cursor) { if (pagination.last) pagination.before = walletAddressIds[cursor] else pagination.after = walletAddressIds[cursor] } - const page = await walletAddressService.getPage(pagination) + const page = await walletAddressService.getPage(pagination, sortOrder) const pageInfo = await getPageInfo( - (pagination) => walletAddressService.getPage(pagination), - page + (pagination) => walletAddressService.getPage(pagination, sortOrder), + page, + sortOrder ) expect(pageInfo).toEqual({ startCursor: walletAddressIds[start], @@ -430,8 +435,8 @@ describe('Open Payments Wallet Address Service', (): void => { describe('getPage', (): void => { getPageTests({ createModel: () => createWalletAddress(deps), - getPage: (pagination?: Pagination) => - walletAddressService.getPage(pagination) + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + walletAddressService.getPage(pagination, sortOrder) }) }) }) diff --git a/packages/backend/src/open_payments/wallet_address/service.ts b/packages/backend/src/open_payments/wallet_address/service.ts index e909365d7a..2aedaee0f4 100644 --- a/packages/backend/src/open_payments/wallet_address/service.ts +++ b/packages/backend/src/open_payments/wallet_address/service.ts @@ -21,7 +21,7 @@ import { IncomingPaymentState } from '../payment/incoming/model' import { IAppConfig } from '../../config/app' -import { Pagination } from '../../shared/baseModel' +import { Pagination, SortOrder } from '../../shared/baseModel' import { WebhookService } from '../../webhook/service' import { poll } from '../../shared/utils' @@ -47,7 +47,10 @@ export interface WalletAddressService { get(id: string): Promise getByUrl(url: string): Promise getOrPollByUrl(url: string): Promise - getPage(pagination?: Pagination): Promise + getPage( + pagination?: Pagination, + sortOrder?: SortOrder + ): Promise processNext(): Promise triggerEvents(limit: number): Promise } @@ -82,7 +85,8 @@ export async function createWalletAddressService({ get: (id) => getWalletAddress(deps, id), getByUrl: (url) => getWalletAddressByUrl(deps, url), getOrPollByUrl: (url) => getOrPollByUrl(deps, url), - getPage: (pagination?) => getWalletAddressPage(deps, pagination), + getPage: (pagination?, sortOrder?) => + getWalletAddressPage(deps, pagination, sortOrder), processNext: () => processNextWalletAddress(deps), triggerEvents: (limit) => triggerWalletAddressEvents(deps, limit) } @@ -223,10 +227,11 @@ async function getWalletAddressByUrl( async function getWalletAddressPage( deps: ServiceDependencies, - pagination?: Pagination + pagination?: Pagination, + sortOrder?: SortOrder ): Promise { return await WalletAddress.query(deps.knex) - .getPage(pagination) + .getPage(pagination, sortOrder) .withGraphFetched('asset') } diff --git a/packages/backend/src/payment-method/ilp/peer/service.test.ts b/packages/backend/src/payment-method/ilp/peer/service.test.ts index 90916d6769..592c7d2625 100644 --- a/packages/backend/src/payment-method/ilp/peer/service.test.ts +++ b/packages/backend/src/payment-method/ilp/peer/service.test.ts @@ -10,7 +10,7 @@ import { IocContract } from '@adonisjs/fold' import { initIocContainer } from '../../..' import { AppServices } from '../../../app' import { Asset } from '../../../asset/model' -import { Pagination } from '../../../shared/baseModel' +import { Pagination, SortOrder } from '../../../shared/baseModel' import { getPageTests } from '../../../shared/baseModel.test' import { createAsset } from '../../../tests/asset' import { createPeer } from '../../../tests/peer' @@ -427,8 +427,8 @@ describe('Peer Service', (): void => { describe('Peer pagination', (): void => { getPageTests({ createModel: () => createPeer(deps, { assetId: asset.id }), - getPage: (pagination: Pagination | undefined) => - peerService.getPage(pagination) + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + peerService.getPage(pagination, sortOrder) }) }) diff --git a/packages/backend/src/payment-method/ilp/peer/service.ts b/packages/backend/src/payment-method/ilp/peer/service.ts index 3771d8287d..a12bfd94d9 100644 --- a/packages/backend/src/payment-method/ilp/peer/service.ts +++ b/packages/backend/src/payment-method/ilp/peer/service.ts @@ -17,7 +17,7 @@ import { import { AssetService } from '../../../asset/service' import { HttpTokenOptions, HttpTokenService } from '../peer-http-token/service' import { HttpTokenError } from '../peer-http-token/errors' -import { Pagination } from '../../../shared/baseModel' +import { Pagination, SortOrder } from '../../../shared/baseModel' import { BaseService } from '../../../shared/baseService' import { isValidHttpUrl } from '../../../shared/utils' import { v4 as uuid } from 'uuid' @@ -65,7 +65,7 @@ export interface PeerService { assetId?: string ): Promise getByIncomingToken(token: string): Promise - getPage(pagination?: Pagination): Promise + getPage(pagination?: Pagination, sortOrder?: SortOrder): Promise addLiquidity( args: AddPeerLiquidityArgs ): Promise @@ -103,7 +103,8 @@ export async function createPeerService({ getByDestinationAddress: (destinationAddress, assetId) => getPeerByDestinationAddress(deps, destinationAddress, assetId), getByIncomingToken: (token) => getPeerByIncomingToken(deps, token), - getPage: (pagination?) => getPeersPage(deps, pagination), + getPage: (pagination?, sortOrder?) => + getPeersPage(deps, pagination, sortOrder), addLiquidity: (args) => addLiquidityById(deps, args), delete: (id) => deletePeer(deps, id) } @@ -371,10 +372,11 @@ async function getPeerByIncomingToken( */ async function getPeersPage( deps: ServiceDependencies, - pagination?: Pagination + pagination?: Pagination, + sortOrder?: SortOrder ): Promise { return await Peer.query(deps.knex) - .getPage(pagination) + .getPage(pagination, sortOrder) .withGraphFetched('asset') } diff --git a/packages/backend/src/shared/baseModel.test.ts b/packages/backend/src/shared/baseModel.test.ts index 2c806ce9d6..9b502ba5d8 100644 --- a/packages/backend/src/shared/baseModel.test.ts +++ b/packages/backend/src/shared/baseModel.test.ts @@ -1,8 +1,8 @@ -import { BaseModel, Pagination } from './baseModel' +import { BaseModel, Pagination, SortOrder } from './baseModel' interface PageTestsOptions { createModel: () => Promise - getPage: (pagination?: Pagination) => Promise + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => Promise } export const getPageTests = ({ @@ -10,6 +10,7 @@ export const getPageTests = ({ getPage }: PageTestsOptions): void => { describe('Common BaseModel pagination', (): void => { + const sortOrder = Math.random() < 0.5 ? SortOrder.Asc : SortOrder.Desc let modelsCreated: Type[] beforeEach(async (): Promise => { @@ -17,6 +18,9 @@ export const getPageTests = ({ for (let i = 0; i < 22; i++) { modelsCreated.push(await createModel()) } + if (sortOrder === SortOrder.Desc) { + modelsCreated.reverse() + } }) test.each` @@ -35,7 +39,7 @@ export const getPageTests = ({ if (pagination?.before !== undefined) { pagination.before = modelsCreated[pagination.before].id } - const models = await getPage(pagination) + const models = await getPage(pagination, sortOrder) expect(models).toHaveLength(expected.length) expect(models[0].id).toEqual(modelsCreated[expected.first].id) expect(models[expected.length - 1].id).toEqual( @@ -49,19 +53,21 @@ export const getPageTests = ({ ${{ first: -1 }} | ${'Pagination index error'} | ${"Can't request less than 0"} ${{ first: 101 }} | ${'Pagination index error'} | ${"Can't request more than 100"} `('$description', async ({ pagination, expectedError }): Promise => { - await expect(getPage(pagination)).rejects.toThrow(expectedError) + await expect(getPage(pagination, sortOrder)).rejects.toThrow( + expectedError + ) }) test('Backwards/Forwards pagination results in same order.', async (): Promise => { const paginationForwards = { first: 10 } - const modelsForwards = await getPage(paginationForwards) + const modelsForwards = await getPage(paginationForwards, sortOrder) const paginationBackwards = { last: 10, before: modelsCreated[10].id } - const modelsBackwards = await getPage(paginationBackwards) + const modelsBackwards = await getPage(paginationBackwards, sortOrder) expect(modelsForwards).toHaveLength(10) expect(modelsBackwards).toHaveLength(10) expect(modelsForwards).toEqual(modelsBackwards) diff --git a/packages/backend/src/shared/baseModel.ts b/packages/backend/src/shared/baseModel.ts index 75ff2fcca3..672772d2f1 100644 --- a/packages/backend/src/shared/baseModel.ts +++ b/packages/backend/src/shared/baseModel.ts @@ -23,6 +23,11 @@ export interface PageInfo { hasPreviousPage: boolean } +export enum SortOrder { + Asc = 'ASC', + Desc = 'DESC' +} + class PaginationQueryBuilder extends QueryBuilder< M, R @@ -45,7 +50,10 @@ class PaginationQueryBuilder extends QueryBuilder< * @param pagination Pagination - cursors and limits. * @returns Model[] An array of Models that form a page. */ - getPage(pagination?: Pagination): this { + getPage( + pagination?: Pagination, + sortOrder: SortOrder = SortOrder.Desc + ): this { const tableName = this.modelClass().tableName if ( typeof pagination?.before === 'undefined' && @@ -57,33 +65,34 @@ class PaginationQueryBuilder extends QueryBuilder< if (first < 0 || first > 100) throw new Error('Pagination index error') const last = pagination?.last || 20 if (last < 0 || last > 100) throw new Error('Pagination index error') - /** * Forward pagination */ if (typeof pagination?.after === 'string') { + const comparisonOperator = sortOrder === SortOrder.Asc ? '>' : '<' return this.whereRaw( - `("${tableName}"."createdAt", "${tableName}"."id") > (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, + `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, [this.modelClass().tableName, pagination.after] ) .orderBy([ - { column: 'createdAt', order: 'asc' }, - { column: 'id', order: 'asc' } + { column: 'createdAt', order: sortOrder }, + { column: 'id', order: sortOrder } ]) .limit(first) } - /** * Backward pagination */ if (typeof pagination?.before === 'string') { + const comparisonOperator = sortOrder === SortOrder.Asc ? '<' : '>' + const order = sortOrder === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc return this.whereRaw( - `("${tableName}"."createdAt", "${tableName}"."id") < (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, + `("${tableName}"."createdAt", "${tableName}"."id") ${comparisonOperator} (select "${tableName}"."createdAt" :: TIMESTAMP, "${tableName}"."id" from ?? where "${tableName}"."id" = ?)`, [this.modelClass().tableName, pagination.before] ) .orderBy([ - { column: 'createdAt', order: 'desc' }, - { column: 'id', order: 'desc' } + { column: 'createdAt', order }, + { column: 'id', order } ]) .limit(last) .runAfter((models) => { @@ -94,8 +103,8 @@ class PaginationQueryBuilder extends QueryBuilder< } return this.orderBy([ - { column: 'createdAt', order: 'asc' }, - { column: 'id', order: 'asc' } + { column: 'createdAt', order: sortOrder }, + { column: 'id', order: sortOrder } ]).limit(first) } } diff --git a/packages/backend/src/shared/pagination.test.ts b/packages/backend/src/shared/pagination.test.ts index d1018342a5..58d20908a9 100644 --- a/packages/backend/src/shared/pagination.test.ts +++ b/packages/backend/src/shared/pagination.test.ts @@ -18,6 +18,7 @@ import { getPageInfo, parsePaginationQueryParameters } from './pagination' import { AssetService } from '../asset/service' import { PeerService } from '../payment-method/ilp/peer/service' import { createPeer } from '../tests/peer' +import { SortOrder } from './baseModel' describe('Pagination', (): void => { let deps: IocContract @@ -26,12 +27,14 @@ describe('Pagination', (): void => { let outgoingPaymentService: OutgoingPaymentService let quoteService: QuoteService let config: IAppConfig + let sortOrder: SortOrder beforeAll(async (): Promise => { config = Config config.publicHost = 'https://wallet.example' deps = await initIocContainer(config) appContainer = await createTestApp(deps) + sortOrder = Math.random() < 0.5 ? SortOrder.Asc : SortOrder.Desc }) afterEach(async (): Promise => { @@ -108,21 +111,27 @@ describe('Pagination', (): void => { }) paymentIds.push(payment.id) } + if (sortOrder === SortOrder.Desc) { + paymentIds.reverse() + } if (cursor) { if (pagination.last) pagination.before = paymentIds[cursor] else pagination.after = paymentIds[cursor] } const page = await incomingPaymentService.getWalletAddressPage({ walletAddressId: defaultWalletAddress.id, - pagination + pagination, + sortOrder }) const pageInfo = await getPageInfo( - (pagination) => + (pagination, sortOrder) => incomingPaymentService.getWalletAddressPage({ walletAddressId: defaultWalletAddress.id, - pagination + pagination, + sortOrder }), - page + page, + sortOrder ) expect(pageInfo).toEqual({ startCursor: paymentIds[start], @@ -163,21 +172,27 @@ describe('Pagination', (): void => { }) paymentIds.push(payment.id) } + if (sortOrder === SortOrder.Desc) { + paymentIds.reverse() + } if (cursor) { if (pagination.last) pagination.before = paymentIds[cursor] else pagination.after = paymentIds[cursor] } const page = await outgoingPaymentService.getWalletAddressPage({ walletAddressId: defaultWalletAddress.id, - pagination + pagination, + sortOrder }) const pageInfo = await getPageInfo( - (pagination) => + (pagination, sortOrder) => outgoingPaymentService.getWalletAddressPage({ walletAddressId: defaultWalletAddress.id, - pagination + pagination, + sortOrder }), - page + page, + sortOrder ) expect(pageInfo).toEqual({ startCursor: paymentIds[start], @@ -218,21 +233,27 @@ describe('Pagination', (): void => { }) quoteIds.push(quote.id) } + if (sortOrder === SortOrder.Desc) { + quoteIds.reverse() + } if (cursor) { if (pagination.last) pagination.before = quoteIds[cursor] else pagination.after = quoteIds[cursor] } const page = await quoteService.getWalletAddressPage({ walletAddressId: defaultWalletAddress.id, - pagination + pagination, + sortOrder }) const pageInfo = await getPageInfo( - (pagination) => + (pagination, sortOrder) => quoteService.getWalletAddressPage({ walletAddressId: defaultWalletAddress.id, - pagination + pagination, + sortOrder }), - page + page, + sortOrder ) expect(pageInfo).toEqual({ startCursor: quoteIds[start], @@ -275,14 +296,18 @@ describe('Pagination', (): void => { const asset = await createAsset(deps) assetIds.push(asset.id) } + if (sortOrder === SortOrder.Desc) { + assetIds.reverse() + } if (cursor) { if (pagination.last) pagination.before = assetIds[cursor] else pagination.after = assetIds[cursor] } - const page = await assetService.getPage(pagination) + const page = await assetService.getPage(pagination, sortOrder) const pageInfo = await getPageInfo( - (pagination) => assetService.getPage(pagination), - page + (pagination) => assetService.getPage(pagination, sortOrder), + page, + sortOrder ) expect(pageInfo).toEqual({ startCursor: assetIds[start], @@ -317,14 +342,19 @@ describe('Pagination', (): void => { const peer = await createPeer(deps) peerIds.push(peer.id) } + if (sortOrder === SortOrder.Desc) { + peerIds.reverse() + } if (cursor) { if (pagination.last) pagination.before = peerIds[cursor] else pagination.after = peerIds[cursor] } - const page = await peerService.getPage(pagination) + const page = await peerService.getPage(pagination, sortOrder) const pageInfo = await getPageInfo( - (pagination) => peerService.getPage(pagination), - page + (pagination, sortOrder) => + peerService.getPage(pagination, sortOrder), + page, + sortOrder ) expect(pageInfo).toEqual({ startCursor: peerIds[start], diff --git a/packages/backend/src/shared/pagination.ts b/packages/backend/src/shared/pagination.ts index 8af77ea58e..5cf7eb3518 100644 --- a/packages/backend/src/shared/pagination.ts +++ b/packages/backend/src/shared/pagination.ts @@ -1,6 +1,6 @@ import { PaginationArgs } from '@interledger/open-payments' -import { BaseModel, PageInfo, Pagination } from './baseModel' +import { BaseModel, PageInfo, Pagination, SortOrder } from './baseModel' export function parsePaginationQueryParameters({ first, @@ -16,8 +16,9 @@ export function parsePaginationQueryParameters({ } export async function getPageInfo( - getPage: (pagination: Pagination) => Promise, - page: T[] + getPage: (pagination: Pagination, sortOrder?: SortOrder) => Promise, + page: T[], + sortOrder?: SortOrder ): Promise { if (page.length == 0) return { @@ -30,18 +31,24 @@ export async function getPageInfo( let hasNextPage, hasPreviousPage try { - hasNextPage = await getPage({ - after: lastId, - first: 1 - }) + hasNextPage = await getPage( + { + after: lastId, + first: 1 + }, + sortOrder + ) } catch (e) { hasNextPage = [] } try { - hasPreviousPage = await getPage({ - before: firstId, - last: 1 - }) + hasPreviousPage = await getPage( + { + before: firstId, + last: 1 + }, + sortOrder + ) } catch (e) { hasPreviousPage = [] } diff --git a/packages/backend/src/webhook/service.test.ts b/packages/backend/src/webhook/service.test.ts index 078339a93d..8b0385163e 100644 --- a/packages/backend/src/webhook/service.test.ts +++ b/packages/backend/src/webhook/service.test.ts @@ -20,7 +20,7 @@ import { IocContract } from '@adonisjs/fold' import { initIocContainer } from '../' import { AppServices } from '../app' import { getPageTests } from '../shared/baseModel.test' -import { Pagination } from '../shared/baseModel' +import { Pagination, SortOrder } from '../shared/baseModel' import { createWebhookEvent, randomWebhookEvent, @@ -35,6 +35,7 @@ describe('Webhook Service', (): void => { let knex: Knex let webhookUrl: URL let event: WebhookEvent + let sortOrder: SortOrder const WEBHOOK_SECRET = 'test secret' async function makeWithdrawalEvent(event: WebhookEvent): Promise { @@ -62,6 +63,7 @@ describe('Webhook Service', (): void => { webhookService = await deps.use('webhookService') accountingService = await deps.use('accountingService') webhookUrl = new URL(Config.webhookUrl) + sortOrder = Math.random() < 0.5 ? SortOrder.Asc : SortOrder.Desc }) afterEach(async (): Promise => { @@ -103,8 +105,8 @@ describe('Webhook Service', (): void => { describe('getPage', (): void => { getPageTests({ createModel: () => createWebhookEvent(deps), - getPage: (pagination?: Pagination) => - webhookService.getPage({ pagination }) + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + webhookService.getPage({ pagination, sortOrder }) }) }) @@ -145,7 +147,8 @@ describe('Webhook Service', (): void => { type: { in: [type] } - } + }, + sortOrder }) const expectedLength = webhookEvents.filter( (event) => event.type === type @@ -159,9 +162,13 @@ describe('Webhook Service', (): void => { const idsOfTypeY = webhookEvents .filter((event) => event.type === type) .map((event) => event.id) + if (sortOrder === SortOrder.Desc) { + idsOfTypeY.reverse() + } const page = await webhookService.getPage({ pagination: { first: 10, after: idsOfTypeY[0] }, - filter + filter, + sortOrder }) expect(page[0].id).toBe(idsOfTypeY[1]) expect(page.filter((event) => event.type === type).length).toBe( diff --git a/packages/backend/src/webhook/service.ts b/packages/backend/src/webhook/service.ts index f1cf25e525..9e8363594f 100644 --- a/packages/backend/src/webhook/service.ts +++ b/packages/backend/src/webhook/service.ts @@ -5,7 +5,7 @@ import { canonicalize } from 'json-canonicalize' import { WebhookEvent } from './model' import { IAppConfig } from '../config/app' import { BaseService } from '../shared/baseService' -import { Pagination } from '../shared/baseModel' +import { Pagination, SortOrder } from '../shared/baseModel' import { FilterString } from '../shared/filters' // First retry waits 10 seconds @@ -20,6 +20,7 @@ interface WebhookEventFilter { interface GetPageOptions { pagination?: Pagination filter?: WebhookEventFilter + sortOrder?: SortOrder } export interface WebhookService { @@ -178,7 +179,7 @@ async function getWebhookEventsPage( deps: ServiceDependencies, options?: GetPageOptions ): Promise { - const { filter, pagination } = options ?? {} + const { filter, pagination, sortOrder } = options ?? {} const query = WebhookEvent.query(deps.knex) @@ -186,5 +187,5 @@ async function getWebhookEventsPage( query.whereIn('type', filter.type.in) } - return await query.getPage(pagination) + return await query.getPage(pagination, sortOrder) } diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index e12110c3c2..cc4fce1e84 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -92,6 +92,7 @@ export type AssetFeesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type AssetEdge = { @@ -893,6 +894,7 @@ export type QueryAssetsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -912,6 +914,7 @@ export type QueryPaymentsArgs = { filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -925,6 +928,7 @@ export type QueryPeersArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -943,6 +947,7 @@ export type QueryWalletAddressesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -952,6 +957,7 @@ export type QueryWebhookEventsArgs = { filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type Quote = { @@ -1056,6 +1062,13 @@ export type SetFeeResponse = MutationResponse & { success: Scalars['Boolean']['output']; }; +export enum SortOrder { + /** Choose ascending order for results. */ + Asc = 'ASC', + /** Choose descending order for results. */ + Desc = 'DESC' +} + export type TransferMutationResponse = MutationResponse & { __typename?: 'TransferMutationResponse'; code: Scalars['String']['output']; @@ -1169,6 +1182,7 @@ export type WalletAddressIncomingPaymentsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -1177,6 +1191,7 @@ export type WalletAddressOutgoingPaymentsArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; @@ -1185,6 +1200,7 @@ export type WalletAddressQuotesArgs = { before?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + sortOrder?: InputMaybe; }; export type WalletAddressEdge = { @@ -1436,6 +1452,7 @@ export type ResolversTypes = { RevokeWalletAddressKeyMutationResponse: ResolverTypeWrapper>; SetFeeInput: ResolverTypeWrapper>; SetFeeResponse: ResolverTypeWrapper>; + SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; TransferMutationResponse: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>;