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
+2
View File
@@ -16,6 +16,8 @@
- fix: |Worker| 地址活跃时间保活增加 1 天写入窗口,用户设置和邮箱访问不再重复更新近期活跃地址,降低 D1 写入量(issue #1103 - fix: |Worker| 地址活跃时间保活增加 1 天写入窗口,用户设置和邮箱访问不再重复更新近期活跃地址,降低 D1 写入量(issue #1103
- feat: |用户系统| 用户绑定地址列表改用服务端分页,并仅在第一页查询总数;用户邮件列表改用 JOIN、删除改用 `EXISTS` 在数据库侧校验地址归属,避免为大用户加载全部绑定地址(issue #1103
### Testing ### Testing
- fix: |E2E| 新增近期地址活跃时间不会被用户设置接口重复写入的回归测试 - fix: |E2E| 新增近期地址活跃时间不会被用户设置接口重复写入的回归测试
+2
View File
@@ -16,6 +16,8 @@
- fix: |Worker| Throttle address-activity touches to one write per day so user settings and mailbox access do not repeatedly update recently active addresses, reducing D1 writes (issue #1103) - fix: |Worker| Throttle address-activity touches to one write per day so user settings and mailbox access do not repeatedly update recently active addresses, reducing D1 writes (issue #1103)
- feat: |User| Add server-side pagination for bound addresses, with totals queried only on the first page; validate user-mail list ownership with a JOIN and delete ownership with `EXISTS` instead of loading every bound address for large users (issue #1103)
### Testing ### Testing
- fix: |E2E| Add regression coverage ensuring user settings do not rewrite recent address activity timestamps - fix: |E2E| Add regression coverage ensuring user settings do not rewrite recent address activity timestamps
@@ -0,0 +1,170 @@
import { test, expect, type APIRequestContext } from '@playwright/test';
import {
WORKER_URL,
createTestAddress,
deleteAddress,
hashPassword,
seedTestMail,
} from '../../fixtures/test-helpers';
async function createUser(request: APIRequestContext) {
const email = `address-page-${Date.now()}@test.example.com`;
const password = hashPassword('test-password-123');
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
data: { email, password },
});
expect(registerRes.ok()).toBe(true);
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
data: { email, password },
});
expect(loginRes.ok()).toBe(true);
const { jwt } = await loginRes.json();
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'));
return { jwt, userId: payload.user_id as number };
}
test.describe('User address pagination', () => {
test('paginates addresses and enforces mail ownership', async ({ request }) => {
const addresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
let outsider: Awaited<ReturnType<typeof createTestAddress>> | undefined;
let originalUserSettings: Record<string, unknown> | undefined;
let userId: number | undefined;
try {
const settingsRes = await request.get(`${WORKER_URL}/admin/user_settings`);
expect(settingsRes.ok()).toBe(true);
originalUserSettings = await settingsRes.json();
const enableUserRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: {
...originalUserSettings,
enable: true,
enableMailVerify: false,
maxAddressCount: 0,
},
});
expect(enableUserRes.ok()).toBe(true);
const user = await createUser(request);
const userJwt = user.jwt;
userId = user.userId;
addresses.push(...await Promise.all([
createTestAddress(request, 'user-page-a'),
createTestAddress(request, 'user-page-b'),
createTestAddress(request, 'user-page-c'),
]));
outsider = await createTestAddress(request, 'user-page-outsider');
for (const item of addresses) {
const bindRes = await request.post(`${WORKER_URL}/user_api/bind_address`, {
headers: {
Authorization: `Bearer ${item.jwt}`,
'x-user-token': userJwt,
},
});
expect(bindRes.ok()).toBe(true);
}
const defaultPageRes = await request.get(`${WORKER_URL}/user_api/bind_address`, {
headers: { 'x-user-token': userJwt },
});
expect(defaultPageRes.ok()).toBe(true);
const defaultPage = await defaultPageRes.json();
expect(defaultPage.count).toBe(3);
expect(defaultPage.results).toHaveLength(3);
const firstPageRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=2&offset=0`,
{ headers: { 'x-user-token': userJwt } },
);
expect(firstPageRes.ok()).toBe(true);
const firstPage = await firstPageRes.json();
expect(firstPage.count).toBe(3);
expect(firstPage.results).toHaveLength(2);
expect(firstPage.results[0].mail_count).toBe(0);
expect(firstPage.results[0].send_count).toBe(0);
expect(firstPage.results[0]).toHaveProperty('source_meta');
expect(firstPage.results[0]).not.toHaveProperty('password');
const secondPageRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=2&offset=2`,
{ headers: { 'x-user-token': userJwt } },
);
expect(secondPageRes.ok()).toBe(true);
const secondPage = await secondPageRes.json();
expect(secondPage.count).toBe(0);
expect(secondPage.results).toHaveLength(1);
const invalidLimitRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=101&offset=0`,
{ headers: { 'x-user-token': userJwt } },
);
expect(invalidLimitRes.status()).toBe(400);
const invalidOffsetRes = await request.get(
`${WORKER_URL}/user_api/bind_address?limit=20&offset=-1`,
{ headers: { 'x-user-token': userJwt } },
);
expect(invalidOffsetRes.status()).toBe(400);
await seedTestMail(request, addresses[0].address, { subject: 'Bound mail' });
await seedTestMail(request, outsider.address, { subject: 'Outsider mail' });
const userMailsRes = await request.get(`${WORKER_URL}/user_api/mails?limit=20&offset=0`, {
headers: { 'x-user-token': userJwt },
});
expect(userMailsRes.ok()).toBe(true);
const userMails = await userMailsRes.json();
expect(userMails.count).toBe(1);
expect(userMails.results[0].address).toBe(addresses[0].address);
const filteredOutsiderMailsRes = await request.get(
`${WORKER_URL}/user_api/mails?limit=20&offset=0&address=${encodeURIComponent(outsider.address)}`,
{ headers: { 'x-user-token': userJwt } },
);
expect(filteredOutsiderMailsRes.ok()).toBe(true);
const filteredOutsiderMails = await filteredOutsiderMailsRes.json();
expect(filteredOutsiderMails.count).toBe(0);
expect(filteredOutsiderMails.results).toHaveLength(0);
const outsiderMailsRes = await request.get(`${WORKER_URL}/api/mails?limit=20&offset=0`, {
headers: { Authorization: `Bearer ${outsider.jwt}` },
});
expect(outsiderMailsRes.ok()).toBe(true);
const outsiderMails = await outsiderMailsRes.json();
expect(outsiderMails.results).toHaveLength(1);
const forbiddenDeleteRes = await request.delete(
`${WORKER_URL}/user_api/mails/${outsiderMails.results[0].id}`,
{ headers: { 'x-user-token': userJwt } },
);
expect(forbiddenDeleteRes.ok()).toBe(true);
const outsiderAfterRes = await request.get(`${WORKER_URL}/api/mails?limit=20&offset=0`, {
headers: { Authorization: `Bearer ${outsider.jwt}` },
});
expect(outsiderAfterRes.ok()).toBe(true);
const outsiderAfter = await outsiderAfterRes.json();
expect(outsiderAfter.results).toHaveLength(1);
} finally {
try {
await Promise.allSettled(
[...addresses, outsider].filter((item) => item !== undefined)
.map((item) => deleteAddress(request, item.jwt)),
);
if (userId !== undefined) {
const deleteUserRes = await request.delete(`${WORKER_URL}/admin/users/${userId}`);
expect(deleteUserRes.ok()).toBe(true);
}
} finally {
if (originalUserSettings) {
const restoreSettingsRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
data: originalUserSettings,
});
expect(restoreSettingsRes.ok()).toBe(true);
}
}
}
});
});
@@ -0,0 +1,135 @@
import { expect, request as apiRequest, test } from '@playwright/test';
import type { APIRequestContext } from '@playwright/test';
import {
FRONTEND_URL,
WORKER_URL,
createTestAddress,
deleteAddress,
hashPassword,
} from '../../fixtures/test-helpers';
async function saveUserSettings(request: APIRequestContext, settings: Record<string, unknown>) {
const response = await request.post(`${WORKER_URL}/admin/user_settings`, { data: settings });
expect(response.ok()).toBe(true);
}
async function createUser(request: APIRequestContext) {
const email = `address-browser-${Date.now()}@test.example.com`;
const password = hashPassword('test-password-123');
const registerResponse = await request.post(`${WORKER_URL}/user_api/register`, {
data: { email, password },
});
expect(registerResponse.ok()).toBe(true);
const loginResponse = await request.post(`${WORKER_URL}/user_api/login`, {
data: { email, password },
});
expect(loginResponse.ok()).toBe(true);
const { jwt } = await loginResponse.json();
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8'));
return { email, jwt, userId: payload.user_id as number };
}
async function bindAddress(request: APIRequestContext, userJwt: string, addressJwt: string) {
const response = await request.post(`${WORKER_URL}/user_api/bind_address`, {
headers: {
Authorization: `Bearer ${addressJwt}`,
'x-user-token': userJwt,
},
});
expect(response.ok()).toBe(true);
}
test.describe('User address pagination browser flow', () => {
test('paginates addresses and filters mail', async ({ page }) => {
test.setTimeout(120_000);
const request = await apiRequest.newContext();
const createdAddresses: Awaited<ReturnType<typeof createTestAddress>>[] = [];
let originalUserSettings: Record<string, unknown> | undefined;
let userId: number | undefined;
try {
const settingsResponse = await request.get(`${WORKER_URL}/admin/user_settings`);
expect(settingsResponse.ok()).toBe(true);
originalUserSettings = await settingsResponse.json();
await saveUserSettings(request, {
...originalUserSettings,
enable: true,
enableMailVerify: false,
maxAddressCount: 0,
});
const user = await createUser(request);
userId = user.userId;
for (let index = 0; index < 21; index += 1) {
const address = await createTestAddress(request, `browser-page-${index}-`);
createdAddresses.push(address);
await bindAddress(request, user.jwt, address.jwt);
}
const defaultAddressPageResponse = await request.get(
`${WORKER_URL}/user_api/bind_address`,
{ headers: { 'x-user-token': user.jwt } },
);
expect(defaultAddressPageResponse.ok()).toBe(true);
const defaultAddressPage = await defaultAddressPageResponse.json();
expect(defaultAddressPage.count).toBe(21);
expect(defaultAddressPage.results).toHaveLength(20);
await page.goto(`${FRONTEND_URL}/en/`);
await page.evaluate((userJwt) => {
localStorage.setItem('userJwt', userJwt);
}, user.jwt);
await page.goto(`${FRONTEND_URL}/en/user`);
await expect(page.getByText(user.email)).toBeVisible({ timeout: 15_000 });
const pagination = page.locator('.n-pagination').first();
const addressRows = page.locator('.n-data-table-tbody .n-data-table-tr');
await expect(pagination).toContainText(/Total:\s*21/);
await expect(addressRows).toHaveCount(20);
await pagination.locator('.n-pagination-item').filter({ hasText: /^2$/ }).click();
await expect(addressRows).toHaveCount(1);
const selectedAddress = createdAddresses[20];
const initialMailboxAddressesResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return url.pathname === '/user_api/bind_address'
&& url.searchParams.get('limit') === '100';
});
await page.getByText('Mail Box', { exact: true }).click();
const initialMailboxResponse = await initialMailboxAddressesResponse;
expect(initialMailboxResponse.ok()).toBe(true);
const mailboxAddressSelect = page.locator('.n-input-group .n-select').first();
await mailboxAddressSelect.click();
const mailboxOptions = page.locator('.n-base-select-menu:visible');
await expect(mailboxOptions).toContainText(selectedAddress.address);
const filteredMailResponse = page.waitForResponse((response) => {
const url = new URL(response.url());
return url.pathname === '/user_api/mails'
&& url.searchParams.get('address') === selectedAddress.address;
});
await mailboxOptions.getByText(selectedAddress.address, { exact: true }).click();
expect((await filteredMailResponse).ok()).toBe(true);
} finally {
try {
try {
await Promise.allSettled(createdAddresses.map((address) => deleteAddress(request, address.jwt)));
if (userId !== undefined) {
await request.delete(`${WORKER_URL}/admin/users/${userId}`);
}
} finally {
if (originalUserSettings) {
await saveUserSettings(request, originalUserSettings);
}
}
} finally {
await request.dispose();
}
}
});
});
+1 -1
View File
@@ -97,7 +97,7 @@ const buildLocalOptions = (excludeAddresses = new Set()) => {
const buildUserOptions = async () => { const buildUserOptions = async () => {
const children = []; const children = [];
try { try {
const { results } = await api.fetch(`/user_api/bind_address`); const { results } = await api.fetch(`/user_api/bind_address?limit=100&offset=0`);
for (const row of results || []) { for (const row of results || []) {
const address = row.address || row.name; const address = row.address || row.name;
if (!address) continue; if (!address) continue;
+4
View File
@@ -852,6 +852,10 @@ export const MESSAGE_REGISTRY = {
"en": "Mail Count", "en": "Mail Count",
"zh": "邮件数量" "zh": "邮件数量"
}, },
"itemCount": {
"en": "Total",
"zh": "总数"
},
"name": { "name": {
"en": "Name", "en": "Name",
"zh": "名称" "zh": "名称"
+38 -5
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, h, onMounted } from 'vue'; import { ref, h, onMounted, watch } from 'vue';
import { useScopedI18n } from '@/i18n/app' import { useScopedI18n } from '@/i18n/app'
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { NBadge, NPopconfirm, NButton } from 'naive-ui' import { NBadge, NPopconfirm, NButton } from 'naive-ui'
@@ -17,6 +17,9 @@ const router = useRouter()
const { locale, t } = useScopedI18n('views.user.AddressManagement') const { locale, t } = useScopedI18n('views.user.AddressManagement')
const data = ref([]) const data = ref([])
const count = ref(0)
const page = ref(1)
const pageSize = ref(20)
const showTranferAddress = ref(false) const showTranferAddress = ref(false)
const currentAddress = ref("") const currentAddress = ref("")
const currentAddressId = ref(0) const currentAddressId = ref(0)
@@ -46,7 +49,11 @@ const unbindAddress = async (address_id) => {
body: JSON.stringify({ address_id }) body: JSON.stringify({ address_id })
}); });
message.success(t('unbindAddress') + " " + t('success')); message.success(t('unbindAddress') + " " + t('success'));
await fetchData(); if (page.value === 1) {
await fetchData();
} else {
page.value = 1;
}
} catch (error) { } catch (error) {
console.log(error) console.log(error)
message.error(error.message || "error"); message.error(error.message || "error");
@@ -71,7 +78,11 @@ const transferAddress = async () => {
}) })
}); });
message.success(t('transferAddress') + " " + t('success')); message.success(t('transferAddress') + " " + t('success'));
await fetchData(); if (page.value === 1) {
await fetchData();
} else {
page.value = 1;
}
showTranferAddress.value = false; showTranferAddress.value = false;
currentAddressId.value = 0; currentAddressId.value = 0;
currentAddress.value = ""; currentAddress.value = "";
@@ -84,10 +95,17 @@ const transferAddress = async () => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const { results } = await api.fetch( const params = new URLSearchParams({
`/user_api/bind_address` limit: String(pageSize.value),
offset: String((page.value - 1) * pageSize.value),
});
const { results, count: addressCount } = await api.fetch(
`/user_api/bind_address?${params.toString()}`
); );
data.value = results; data.value = results;
if (page.value === 1) {
count.value = addressCount;
}
} catch (error) { } catch (error) {
console.log(error) console.log(error)
message.error(error.message || "error"); message.error(error.message || "error");
@@ -178,6 +196,10 @@ const columns = [
onMounted(async () => { onMounted(async () => {
await fetchData() await fetchData()
}) })
watch([page, pageSize], async () => {
await fetchData();
})
</script> </script>
<template> <template>
@@ -197,6 +219,12 @@ onMounted(async () => {
<n-tabs type="segment"> <n-tabs type="segment">
<n-tab-pane name="address" :tab="t('address')"> <n-tab-pane name="address" :tab="t('address')">
<div class="address-table-scroll"> <div class="address-table-scroll">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
:page-sizes="[20, 50, 100]" show-size-picker>
<template #prefix="{ itemCount }">
{{ t('itemCount') }}: {{ itemCount }}
</template>
</n-pagination>
<n-data-table :columns="columns" :data="data" :bordered="false" embedded /> <n-data-table :columns="columns" :data="data" :bordered="false" embedded />
</div> </div>
</n-tab-pane> </n-tab-pane>
@@ -216,4 +244,9 @@ onMounted(async () => {
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
} }
.n-pagination {
margin-top: 10px;
margin-bottom: 10px;
}
</style> </style>
+1 -1
View File
@@ -32,7 +32,7 @@ const fetchMailData = async (limit, offset) => {
const fetchAddresData = async () => { const fetchAddresData = async () => {
try { try {
const { results } = await api.fetch( const { results } = await api.fetch(
`/user_api/bind_address` `/user_api/bind_address?limit=100&offset=0`
); );
addressFilterOptions.value = results.map((item) => { addressFilterOptions.value = results.map((item) => {
return { return {
@@ -160,6 +160,36 @@ This endpoint uses **User JWT** (obtained via `/user_api/login` or `/user_api/re
- User JWT uses `x-user-token: <jwt>` to access `/user_api/*` endpoints - User JWT uses `x-user-token: <jwt>` to access `/user_api/*` endpoints
::: :::
### Bound Address List
`GET /user_api/bind_address` uses server-side pagination and accepts these query parameters:
Requests without pagination parameters return the default first page. Fetching all bound addresses in one request is not supported.
| Parameter | Default | Description |
| --- | --- | --- |
| `limit` | `20` | Page size, from 1 to 100 |
| `offset` | `0` | Pagination offset |
The `results` array contains only the current page. The total is queried only when `offset=0`; later pages return `count: 0`, so clients should retain the total from the first page.
```python
import requests
url = "https://<your-worker-address>/user_api/bind_address"
headers = {
"x-user-token": "<your-user-JWT-token>",
}
querystring = {
"limit": "20",
"offset": "0",
}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())
```
### User Mail List
Supports `address` filter Supports `address` filter
```python ```python
@@ -160,6 +160,36 @@ print(response.json())
- 用户 JWT 使用 `x-user-token: <jwt>` 访问 `/user_api/*` 接口 - 用户 JWT 使用 `x-user-token: <jwt>` 访问 `/user_api/*` 接口
::: :::
### 用户绑定地址列表
`GET /user_api/bind_address` 使用服务端分页,支持以下查询参数:
未携带分页参数时返回默认第一页,不支持一次获取全部绑定地址。
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `limit` | `20` | 每页数量,范围为 1–100 |
| `offset` | `0` | 分页偏移量 |
响应中的 `results` 仅包含当前页。仅 `offset=0` 时查询总数,后续页面的 `count``0`,客户端应保留第一页返回的总数。
```python
import requests
url = "https://<你的worker地址>/user_api/bind_address"
headers = {
"x-user-token": "<你的用户JWT Token>",
}
querystring = {
"limit": "20",
"offset": "0",
}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())
```
### 用户邮件列表
支持 `address` 过滤 支持 `address` 过滤
```python ```python
+19 -11
View File
@@ -4,7 +4,7 @@ import { Jwt } from 'hono/utils/jwt'
import { isAddressCountLimitReached } from "../utils" import { isAddressCountLimitReached } from "../utils"
import { unbindTelegramByAddress } from '../telegram_api/common'; import { unbindTelegramByAddress } from '../telegram_api/common';
import i18n from '../i18n'; import i18n from '../i18n';
import { updateAddressUpdatedAt, commonGetUserRole, hideObjectFields } from '../common'; import { updateAddressUpdatedAt, commonGetUserRole, handleListQuery, hideObjectFields } from '../common';
const UserBindAddressModule = { const UserBindAddressModule = {
bind: async (c: Context<HonoCustomType>) => { bind: async (c: Context<HonoCustomType>) => {
@@ -97,16 +97,24 @@ const UserBindAddressModule = {
}, },
getBindedAddresses: async (c: Context<HonoCustomType>) => { getBindedAddresses: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const results = await UserBindAddressModule.getBindedAddressesById(c, user_id); const { limit, offset } = c.req.query();
return c.json({ const params = [String(user_id)];
results: results, const fromQuery = ` FROM address a`
}); + ` JOIN users_address ua ON ua.address_id = a.id`
}, + ` WHERE ua.user_id = ?`;
getBindedAddressListById: async ( return await handleListQuery(
c: Context<HonoCustomType>, user_id: number | string c,
): Promise<string[]> => { `SELECT a.*,`
const bindedAddressList = await UserBindAddressModule.getBindedAddressesById(c, user_id); + ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,`
return bindedAddressList.map((item) => item.name); + ` (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 ( getBindedAddressesById: async (
c: Context<HonoCustomType>, user_id: number | string c: Context<HonoCustomType>, user_id: number | string
+18 -19
View File
@@ -1,30 +1,26 @@
import { Context } from "hono"; import { Context } from "hono";
import i18n from "../i18n"; import i18n from "../i18n";
import { handleMailListQuery } from "../common"; import { handleMailListQuery } from "../common";
import UserBindAddressModule from "./bind_address";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
export default { export default {
getMails: async (c: Context<HonoCustomType>) => { getMails: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const { address, limit, offset } = c.req.query(); const { address, limit, offset } = c.req.query();
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id); const filterQuerys = [`ua.user_id = ?`];
const addressList = address ? bindedAddressList.filter((item) => item == address) : bindedAddressList; const filterParams = [String(user_id)];
const addressQuery = `address IN (${addressList.map(() => "?").join(",")})`; if (address) {
const addressParams = addressList; filterQuerys.push(`rm.address = ?`);
filterParams.push(address);
// user must have at least one binded address to query mails
if (addressList.length <= 0) {
return c.json({ results: [], count: 0 });
} }
const fromQuery = ` FROM users_address ua`
const filterQuerys = [addressQuery].filter((item) => item).join(" and "); + ` JOIN address a ON a.id = ua.address_id`
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : ""; + ` JOIN raw_mails rm ON rm.address = a.name`
const filterParams = [...addressParams] + ` WHERE ${filterQuerys.join(" AND ")}`;
return await handleMailListQuery(c, return await handleMailListQuery(c,
`SELECT * FROM raw_mails ${finalQuery}`, `SELECT rm.*${fromQuery}`,
`SELECT count(*) as count FROM raw_mails ${finalQuery}`, `SELECT count(*) as count${fromQuery}`,
filterParams, limit, offset filterParams, limit, offset, 'rm.id desc'
); );
}, },
deleteMail: async (c: Context<HonoCustomType>) => { deleteMail: async (c: Context<HonoCustomType>) => {
@@ -34,11 +30,14 @@ export default {
} }
const { id } = c.req.param(); const { id } = c.req.param();
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE id = ?` `DELETE FROM raw_mails WHERE id = ?`
+ ` and address IN (${bindedAddressList.map(() => "?").join(",")})` + ` AND EXISTS (`
).bind(id, ...bindedAddressList).run(); + `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({ return c.json({
success: success success: success
}) })