Skip to content

Commit

Permalink
feat: invoice pagination (#3512)
Browse files Browse the repository at this point in the history
- add invoice pagination to account and wallet gql objects
  • Loading branch information
UncleSamtoshi authored Nov 8, 2023
1 parent 94d275e commit 6232ee2
Show file tree
Hide file tree
Showing 31 changed files with 1,173 additions and 40 deletions.
95 changes: 95 additions & 0 deletions core/api/dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ interface Account
defaultWalletId: WalletId!
displayCurrency: DisplayCurrency!
id: ID!
invoices(
"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
walletIds: [WalletId]
): InvoiceConnection
level: AccountLevel!
limits: AccountLimits!
notificationSettings: NotificationSettings!
Expand Down Expand Up @@ -220,6 +234,21 @@ type BTCWallet implements Wallet
id: ID!
invoiceByPaymentHash(paymentHash: PaymentHash!): Invoice!

"""A list of all invoices associated with walletIds optionally passed."""
invoices(
"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
): InvoiceConnection

"""An unconfirmed incoming onchain balance."""
pendingIncomingBalance: SignedAmount!
pendingTransactions: [Transaction!]!
Expand Down Expand Up @@ -347,6 +376,22 @@ type ConsumerAccount implements Account
defaultWalletId: WalletId!
displayCurrency: DisplayCurrency!
id: ID!

"""A list of all invoices associated with walletIds optionally passed."""
invoices(
"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
walletIds: [WalletId]
): InvoiceConnection
level: AccountLevel!
limits: AccountLimits!
notificationSettings: NotificationSettings!
Expand Down Expand Up @@ -614,6 +659,28 @@ interface Invoice
paymentStatus: InvoicePaymentStatus!
}

"""A connection to a list of items."""
type InvoiceConnection
@join__type(graph: PUBLIC)
{
"""A list of edges."""
edges: [InvoiceEdge!]

"""Information to aid in pagination."""
pageInfo: PageInfo!
}

"""An edge in a connection."""
type InvoiceEdge
@join__type(graph: PUBLIC)
{
"""A cursor for use in pagination"""
cursor: String!

"""The item at the end of the edge"""
node: Invoice!
}

enum InvoicePaymentStatus
@join__type(graph: PUBLIC)
{
Expand Down Expand Up @@ -1661,6 +1728,21 @@ type UsdWallet implements Wallet
id: ID!
invoiceByPaymentHash(paymentHash: PaymentHash!): Invoice!

"""A list of all invoices associated with walletIds optionally passed."""
invoices(
"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
): InvoiceConnection

"""An unconfirmed incoming onchain balance."""
pendingIncomingBalance: SignedAmount!
pendingTransactions: [Transaction!]!
Expand Down Expand Up @@ -1975,6 +2057,19 @@ interface Wallet
"""
paymentHash: PaymentHash!
): Invoice!
invoices(
"""Returns the items in the list that come after the specified cursor."""
after: String

"""Returns the items in the list that come before the specified cursor."""
before: String

"""Returns the first n items from the list."""
first: Int

"""Returns the last n items from the list."""
last: Int
): InvoiceConnection
pendingIncomingBalance: SignedAmount!

"""
Expand Down
30 changes: 30 additions & 0 deletions core/api/src/app/accounts/get-invoices-for-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getInvoicesForWallets } from "../wallets"

import { RepositoryError } from "@/domain/errors"
import { WalletsRepository } from "@/services/mongoose"

export const getInvoicesForAccountByWalletIds = async ({
account,
walletIds,
rawPaginationArgs,
}: {
account: Account
walletIds?: WalletId[]
rawPaginationArgs: {
first?: number | null
last?: number | null
before?: string | null
after?: string | null
}
}): Promise<PaginatedQueryResult<WalletInvoice> | ApplicationError> => {
const walletsRepo = WalletsRepository()

const accountWallets = await walletsRepo.listByAccountId(account.id)
if (accountWallets instanceof RepositoryError) return accountWallets

const wallets = accountWallets.filter((wallet) =>
walletIds ? walletIds.includes(wallet.id) : true,
)

return getInvoicesForWallets({ wallets, rawPaginationArgs })
}
1 change: 1 addition & 0 deletions core/api/src/app/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from "./enable-notification-category"
export * from "./enable-notification-channel"
export * from "./disable-notification-channel"
export * from "./get-pending-onchain-transactions-for-account"
export * from "./get-invoices-for-account"

const accounts = AccountsRepository()

Expand Down
32 changes: 32 additions & 0 deletions core/api/src/app/wallets/get-invoices-for-wallets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MAX_PAGINATION_PAGE_SIZE } from "@/config"
import { checkedToPaginatedQueryArgs } from "@/domain/primitives"
import { WalletInvoicesRepository } from "@/services/mongoose"

export const getInvoicesForWallets = async ({
wallets,
rawPaginationArgs,
}: {
wallets: Wallet[]
rawPaginationArgs: {
first?: number | null
last?: number | null
before?: string | null
after?: string | null
}
}): Promise<PaginatedQueryResult<WalletInvoice> | ApplicationError> => {
const walletIds = wallets.map((wallet) => wallet.id)

const paginationArgs = checkedToPaginatedQueryArgs({
paginationArgs: rawPaginationArgs,
maxPageSize: MAX_PAGINATION_PAGE_SIZE,
})

if (paginationArgs instanceof Error) {
return paginationArgs
}

return WalletInvoicesRepository().findInvoicesForWallets({
walletIds,
paginationArgs,
})
}
1 change: 1 addition & 0 deletions core/api/src/app/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from "./validate"
export * from "./get-invoice-for-wallet-by-hash"
export * from "./get-pending-onchain-transactions-for-wallets"
export * from "./get-pending-transactions-by-addresses"
export * from "./get-invoices-for-wallets"

import { WalletsRepository } from "@/services/mongoose"

Expand Down
2 changes: 2 additions & 0 deletions core/api/src/config/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export const MEMO_SHARING_SATS_THRESHOLD = yamlConfig.spamLimits
export const MEMO_SHARING_CENTS_THRESHOLD = yamlConfig.spamLimits
.memoSharingCentsThreshold as UsdCents

export const MAX_PAGINATION_PAGE_SIZE = 100

// how many block are we looking back for getChainTransactions
const getOnChainScanDepth = (val: number): ScanDepth => {
const scanDepth = checkedToScanDepth(val)
Expand Down
2 changes: 2 additions & 0 deletions core/api/src/domain/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export class InactiveAccountError extends InvalidAccountStatusError {}

export class InvalidMinutesError extends ValidationError {}

export class InvalidPaginatedQueryArgsError extends ValidationError {}

export class InvalidNonHodlInvoiceError extends ValidationError {
level = ErrorLevel.Critical
}
Expand Down
76 changes: 74 additions & 2 deletions core/api/src/domain/primitives/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { InvalidMinutesError } from "@/domain/errors"

import { InvalidMinutesError, InvalidPaginatedQueryArgsError } from "@/domain/errors"
export const toSeconds = (seconds: number): Seconds => {
return seconds as Seconds
}
Expand All @@ -13,3 +12,76 @@ export const checkedToMinutes = (minutes: number): Minutes | ValidationError =>
if (!isMinutes) return new InvalidMinutesError(`Invalid value for minutes: ${minutes}`)
return minutes as Minutes
}

export const checkedToPaginatedQueryCursor = (cursor: string): PaginatedQueryCursor => {
return cursor as PaginatedQueryCursor
}

export const checkedToPaginatedQueryArgs = ({
paginationArgs,
maxPageSize,
}: {
paginationArgs: {
first?: number | null
last?: number | null
before?: string | null
after?: string | null
}
maxPageSize: number
}): InvalidPaginatedQueryArgsError | PaginatedQueryArgs => {
const { first, last, before, after } = paginationArgs || {}
if (first && last) {
return new InvalidPaginatedQueryArgsError(`Cannot use both "first" and "last".`)
}

const afterCursor =
typeof after === "string" ? checkedToPaginatedQueryCursor(after) : undefined
const beforeCursor =
typeof before === "string" ? checkedToPaginatedQueryCursor(before) : undefined

if (typeof first === "number") {
if (first > maxPageSize) {
return new InvalidPaginatedQueryArgsError(
`Requested page size (${first}) is greater than max allowed page size ${maxPageSize}.`,
)
}

if (first <= 0) {
return new InvalidPaginatedQueryArgsError(
`Requested page size (${first}) must be greater than 0.`,
)
}

return {
first,
after: afterCursor,
before: beforeCursor,
}
}

if (typeof last === "number") {
if (last > maxPageSize) {
return new InvalidPaginatedQueryArgsError(
`Requested page size (${last}) is greater than max allowed page size ${maxPageSize}.`,
)
}

if (last <= 0) {
return new InvalidPaginatedQueryArgsError(
`Requested page size (${last}) must be greater than 0.`,
)
}

return {
last,
after: afterCursor,
before: beforeCursor,
}
}

return {
first: maxPageSize,
after: afterCursor,
before: beforeCursor,
}
}
27 changes: 27 additions & 0 deletions core/api/src/domain/primitives/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,30 @@ type JSONValue =
| undefined
type JSONArray = Array<JSONValue>
type JSONObject = { [key: string]: JSONValue }

type PaginatedQueryCursor = string & { readonly brand: unique symbol }
type PaginatedQueryArgs =
| {
first: number
last?: undefined
after?: PaginatedQueryCursor
before?: PaginatedQueryCursor
}
| {
first?: undefined
last: number
before?: PaginatedQueryCursor
after?: PaginatedQueryCursor
}
type PaginatedQueryResult<T> = {
edges: {
node: T
cursor: PaginatedQueryCursor
}[]
pageInfo: {
startCursor?: PaginatedQueryCursor
endCursor?: PaginatedQueryCursor
hasNextPage: boolean
hasPreviousPage: boolean
}
}
8 changes: 8 additions & 0 deletions core/api/src/domain/wallet-invoices/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ interface IWalletInvoicesRepository {
paymentHash: PaymentHash,
) => Promise<WalletInvoiceWithOptionalLnInvoice | RepositoryError>

findInvoicesForWallets: ({
walletIds,
paginationArgs,
}: {
walletIds: WalletId[]
paginationArgs: PaginatedQueryArgs
}) => Promise<PaginatedQueryResult<WalletInvoice> | RepositoryError>

findByPaymentHash: (
paymentHash: PaymentHash,
) => Promise<WalletInvoice | RepositoryError>
Expand Down
Loading

0 comments on commit 6232ee2

Please sign in to comment.