Skip to content
Snippets Groups Projects
Commit 45155661 authored by Aaron Dötsch's avatar Aaron Dötsch
Browse files

Fix pagination when fetching user history

Until now the timestamp of the last listed item was used to query the next entries with an earlier
timestamp. That works well most of the time but sometimes it can happen that entries have the
exact same timestamp, especially for item transactions for when you buy multiple products at once.
Now it is using the ids for each type respectively and not the timestamp anymore, which should
make it work perfectly.
parent d9c482e7
Branches
No related tags found
No related merge requests found
......@@ -17,11 +17,14 @@
function fetchMoreTransactions(){
if(!hasMoreTransactions || isFetching) return;
isFetching = true;
fetch("/api/transactions?" + new URLSearchParams({
fetch("/api/transactions?" + new URLSearchParams(Object.fromEntries(Object.entries({
userId,
before: transactions[transactions.length-1].createdAt.getTime(),
balance: transactions[transactions.length-1].balance-transactions[transactions.length-1].difference
}), {
balance: transactions[transactions.length-1].balance-transactions[transactions.length-1].difference,
lastItemTransactionId: transactions.findLast(t=>t.type=="item")?.id,
lastMoneyTransactionId: transactions.findLast(t=>t.type=="transaction")?.id,
lastMoneyTransferId: transactions.findLast(t=>t.type=="transfer")?.id,
lastVoucherUseTime: transactions.findLast(t=>t.type=="voucher")?.id
}).filter(([,value])=>value!==undefined))), {
method: "GET"
}).then(r=>r.text()).then(devalue.parse).then(({transactions: prevTransactions, hasMore})=>{
if(prevTransactions.length > 0) transactions = [...transactions, ...prevTransactions];
......
import { fetchTransactions } from '../../../../api/transactions.js';
import { getUser } from '../../../api/users.js';
import { fetchTransactions } from '../../../../api/transactions';
import { getUser } from '../../../api/users';
export async function load(event) {
const userId = parseInt(event.params.id);
const user = await getUser(userId);
const { transactions, hasMore } = await fetchTransactions(userId, user.balance, Date.now(), 100);
const { transactions, hasMore } = await fetchTransactions(userId, user.balance, {}, 100);
return { transactions, hasMore, user };
};
......@@ -117,15 +117,24 @@ export async function GET(req){
try {
const userId = parseInt(req.url.searchParams.get("userId"));
const balance = parseInt(req.url.searchParams.get("balance"));
const before = parseInt(req.url.searchParams.get("before"));
if (isNaN(userId) || isNaN(balance) || isNaN(before)) {
const lastItemTransactionId = req.url.searchParams.get("lastItemTransactionId");
const lastMoneyTransactionId = req.url.searchParams.get("lastMoneyTransactionId");
const lastMoneyTransferId = req.url.searchParams.get("lastMoneyTransferId");
const lastVoucherUseId = req.url.searchParams.get("lastVoucherUseId");
const beforeIds = {
itemTransactions: lastItemTransactionId ? parseInt(lastItemTransactionId) : undefined,
moneyTransactions: lastMoneyTransactionId ? parseInt(lastMoneyTransactionId) : undefined,
moneyTransfers: lastMoneyTransferId ? parseInt(lastMoneyTransferId) : undefined,
voucherUses: lastVoucherUseId ? parseInt(lastVoucherUseId) : undefined
}
if (isNaN(userId) || isNaN(balance) || Object.values(beforeIds).some(id => id!==undefined && isNaN(id))) {
return new Response(JSON.stringify({ message: "Invalid fields" }), { status: 400 });
}
if(userId !== req.locals.user.id && !(req.locals.user.perms & Flags.ADMIN)){
return new Response(JSON.stringify({ message: "You can't view other users' transactions" }), { status: 403 });
}
try {
const transactions = await fetchTransactions(userId, balance, before, 100);
const transactions = await fetchTransactions(userId, balance, beforeIds, 100);
// use devalue because JSON.stringify/JSON.parse doesn't really support Date
return new Response(devalue.stringify(transactions), { status: 200 });
} catch (error) {
......
......@@ -94,45 +94,50 @@ export async function useVoucher(userId, voucherCode){
});
}
export async function getMoneyTransactions(userId, before=Date.now(), limit=100) {
export async function getMoneyTransactions(userId, beforeId?: number, limit=100) {
return db.transaction.findMany({
take: limit,
where: { userId, createdAt: { lt: new Date(before) } },
orderBy: { createdAt: "desc" }
where: { userId, id: { lt: beforeId } },
orderBy: { id: "desc" }
});
}
export async function getItemTransactions(userId, before=Date.now(), limit=100) {
export async function getItemTransactions(userId, beforeId?: number, limit=100) {
return db.itemTransaction.findMany({
take: limit,
where: { userId, createdAt: { lt: new Date(before) } },
orderBy: { createdAt: "desc" },
where: { userId, id: { lt: beforeId } },
orderBy: { id: "desc" },
include: { item: true }
});
}
export async function getMoneyTransfers(userId, before=Date.now(), limit=100) {
export async function getMoneyTransfers(userId, beforeId?: number, limit=100) {
return db.moneyTransfer.findMany({
take: limit,
where: { OR: [{ fromId: userId }, { toId: userId }], createdAt: { lt: new Date(before) } },
orderBy: { createdAt: "desc" },
where: { OR: [{ fromId: userId }, { toId: userId }], id: { lt: beforeId } },
orderBy: { id: "desc" },
include: { from: true, to: true }
});
}
export async function getUsedVouchers(usedById, before=Date.now(), limit=100) {
export async function getUsedVouchers(usedById, beforeId: number, limit=100) {
const args: {skip: 1, cursor: {id: number}}|{} = beforeId ? { skip: 1, cursor: { id: beforeId } } : {}; // skip cursor if present
return db.voucher.findMany({
take: limit,
where: { usedById, createdAt: { lt: new Date(before) } },
orderBy: { usedAt: "desc" }
...args,
where: { usedById },
orderBy: [{ id: "desc" }, { usedAt: "desc" }] // in case usedAt is the same for multiple vouchers, order by id to make sure the order is consistent
});
}
// TODO somehow fix this
// this skips transactions if `before` is the same as the createdAt date of the previous transaction
// e.g. you could sort by id instead of createdAt and provide the last id of each transaction type as
// replacement for `before`
export async function fetchTransactions(userId, balanceAfterwards, before, limit) {
type BeforeIds = {
moneyTransactions?: number,
itemTransactions?: number,
moneyTransfers?: number,
voucherUses?: number
};
export async function fetchTransactions(userId: number, balanceAfterwards: number, before: BeforeIds, limit: number) {
/*
For every type of transaction that changes the user's balance the limit of transactions to return
is used. This means at most we fetch 4*limit transactions from the database.
......@@ -149,32 +154,36 @@ export async function fetchTransactions(userId, balanceAfterwards, before, limit
/*
[limit]+1 items are fetched because then we can check if there are more items to fetch.
*/
const moneyTransactions = (await getMoneyTransactions(userId, before, limit+1)).map(transaction => ({
const moneyTransactions = (await getMoneyTransactions(userId, before.moneyTransactions, limit+1)).map(transaction => ({
type: "transaction",
name: transaction.amount > 0 ? "Aufladen" : "Auszahlung",
difference: transaction.amount,
createdAt: transaction.createdAt,
verified: !!transaction.verifiedById
verified: !!transaction.verifiedById,
id: transaction.id
}));
const itemTransactions = (await getItemTransactions(userId, before, limit+1)).map(transaction => ({
const itemTransactions = (await getItemTransactions(userId, before.itemTransactions, limit+1)).map(transaction => ({
type: "item",
name: transaction.item.name,
difference: -(transaction.price + transaction.premium),
premium: transaction.premium,
createdAt: transaction.createdAt,
image: transaction.item.image,
id: transaction.id
}));
const moneyTransfers = (await getMoneyTransfers(userId, before, limit+1)).map(transfer => ({
const moneyTransfers = (await getMoneyTransfers(userId, before.moneyTransactions, limit+1)).map(transfer => ({
type: "transfer",
name: transfer.fromId === userId ? `Gesendet an ${transfer.to.name}` : `Empfangen von ${transfer.from.name}`,
difference: transfer.fromId === userId ? -transfer.amount : transfer.amount,
createdAt: transfer.createdAt
createdAt: transfer.createdAt,
id: transfer.id
}));
const voucherUses = (await getUsedVouchers(userId, before, limit+1)).map(voucher => ({
const voucherUses = (await getUsedVouchers(userId, before.voucherUses, limit+1)).map(voucher => ({
type: "voucher",
name: `Gutschein ${voucher.code} eingelöst`,
difference: voucher.amount,
createdAt: voucher.usedAt // use usedAt because voucher creation time is not relevant
createdAt: voucher.usedAt, // use usedAt because voucher creation time is not relevant
id: voucher.id
}));
let transactions = [...moneyTransactions, ...itemTransactions, ...moneyTransfers, ...voucherUses]
.sort((a, b) => b.createdAt - a.createdAt);
......
......@@ -3,6 +3,6 @@ import { fetchTransactions } from "../api/transactions";
export async function load(event) {
const userId = event.locals.user.id;
const balance = event.locals.user.balance;
const {transactions, hasMore} = await fetchTransactions(userId, balance, Date.now(), 100);
const {transactions, hasMore} = await fetchTransactions(userId, balance, {}, 100);
return { transactions, hasMore, userId };
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment