perf: paginate user addresses and optimize ownership queries (#1105)

* perf: paginate user addresses and optimize mail ownership queries

* docs: document user address pagination

* fix: address pagination review feedback

* fix: cover user address pagination flows

* test: fix user mailbox tab selector

* test: stabilize remote address search flow

* test: stabilize user address browser flow

* fix: preserve address pagination compatibility

* refactor: simplify bound address query types

* refactor: reuse list query for bound addresses

* fix: preserve paginated address totals

* fix: preserve bound address helper contracts

* fix: require pagination for user addresses

* refactor: keep shared pagination behavior unchanged

* fix: align pagination docs and tests

* fix: clear stale address selections

* refactor: simplify user address pagination

* refactor: limit user address changes to pagination

* test: select a visible mailbox address

* fix: preserve bound address response fields
This commit is contained in:
Dream Hunter
2026-08-09 19:23:41 +08:00
committed by GitHub
parent f9281818e9
commit a09ede8944
12 changed files with 450 additions and 37 deletions
+19 -11
View File
@@ -4,7 +4,7 @@ import { Jwt } from 'hono/utils/jwt'
import { isAddressCountLimitReached } from "../utils"
import { unbindTelegramByAddress } from '../telegram_api/common';
import i18n from '../i18n';
import { updateAddressUpdatedAt, commonGetUserRole, hideObjectFields } from '../common';
import { updateAddressUpdatedAt, commonGetUserRole, handleListQuery, hideObjectFields } from '../common';
const UserBindAddressModule = {
bind: async (c: Context<HonoCustomType>) => {
@@ -97,16 +97,24 @@ const UserBindAddressModule = {
},
getBindedAddresses: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload");
const results = await UserBindAddressModule.getBindedAddressesById(c, user_id);
return c.json({
results: results,
});
},
getBindedAddressListById: async (
c: Context<HonoCustomType>, user_id: number | string
): Promise<string[]> => {
const bindedAddressList = await UserBindAddressModule.getBindedAddressesById(c, user_id);
return bindedAddressList.map((item) => item.name);
const { limit, offset } = c.req.query();
const params = [String(user_id)];
const fromQuery = ` FROM address a`
+ ` JOIN users_address ua ON ua.address_id = a.id`
+ ` WHERE ua.user_id = ?`;
return await handleListQuery(
c,
`SELECT a.*,`
+ ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
+ ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count`
+ fromQuery,
`SELECT COUNT(*) AS count${fromQuery}`,
params,
limit ?? 20,
offset ?? 0,
'a.id DESC',
['password'],
);
},
getBindedAddressesById: async (
c: Context<HonoCustomType>, user_id: number | string
+18 -19
View File
@@ -1,30 +1,26 @@
import { Context } from "hono";
import i18n from "../i18n";
import { handleMailListQuery } from "../common";
import UserBindAddressModule from "./bind_address";
import { getBooleanValue } from "../utils";
export default {
getMails: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload");
const { address, limit, offset } = c.req.query();
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
const addressList = address ? bindedAddressList.filter((item) => item == address) : bindedAddressList;
const addressQuery = `address IN (${addressList.map(() => "?").join(",")})`;
const addressParams = addressList;
// user must have at least one binded address to query mails
if (addressList.length <= 0) {
return c.json({ results: [], count: 0 });
const filterQuerys = [`ua.user_id = ?`];
const filterParams = [String(user_id)];
if (address) {
filterQuerys.push(`rm.address = ?`);
filterParams.push(address);
}
const filterQuerys = [addressQuery].filter((item) => item).join(" and ");
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
const filterParams = [...addressParams]
const fromQuery = ` FROM users_address ua`
+ ` JOIN address a ON a.id = ua.address_id`
+ ` JOIN raw_mails rm ON rm.address = a.name`
+ ` WHERE ${filterQuerys.join(" AND ")}`;
return await handleMailListQuery(c,
`SELECT * FROM raw_mails ${finalQuery}`,
`SELECT count(*) as count FROM raw_mails ${finalQuery}`,
filterParams, limit, offset
`SELECT rm.*${fromQuery}`,
`SELECT count(*) as count${fromQuery}`,
filterParams, limit, offset, 'rm.id desc'
);
},
deleteMail: async (c: Context<HonoCustomType>) => {
@@ -34,11 +30,14 @@ export default {
}
const { id } = c.req.param();
const { user_id } = c.get("userPayload");
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
const { success } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE id = ?`
+ ` and address IN (${bindedAddressList.map(() => "?").join(",")})`
).bind(id, ...bindedAddressList).run();
+ ` AND EXISTS (`
+ `SELECT 1 FROM users_address ua`
+ ` JOIN address a ON a.id = ua.address_id`
+ ` WHERE ua.user_id = ? AND a.name = raw_mails.address`
+ `)`
).bind(id, user_id).run();
return c.json({
success: success
})