diff --git a/CHANGELOG.md b/CHANGELOG.md index 8571da5..c6184e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ ### Bug Fixes +- fix: |Admin| 修复 `/admin/address` 与 `/admin/users` 在使用完整邮箱(query 长度超过 50 字节)作为搜索条件时报错 `D1_ERROR: LIKE or GLOB pattern too complex` 的问题,长查询自动改用 `instr()` 绕开 D1 的 LIKE pattern 长度限制(#956) + ### Improvements - docs: |发送邮件 API| 明确 `/api/send_mail` 与 `/external/api/send_mail` 两个端点的认证方式差异,补充"地址 JWT"概念说明(#922) diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index f5c091a..4ff3c03 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -12,6 +12,8 @@ ### Bug Fixes +- fix: |Admin| Fix `D1_ERROR: LIKE or GLOB pattern too complex` on `/admin/address` and `/admin/users` when searching by full email address (query length pushes the LIKE pattern over D1's 50-byte limit). Long queries now fall back to `instr()` to bypass the LIKE pattern length cap (#956) + ### Improvements - docs: |Send Mail API| Clarify authentication differences between `/api/send_mail` and `/external/api/send_mail`, add "Address JWT" concept explanation (#922) diff --git a/e2e/tests/api/admin-address-query.spec.ts b/e2e/tests/api/admin-address-query.spec.ts new file mode 100644 index 0000000..de5a9a1 --- /dev/null +++ b/e2e/tests/api/admin-address-query.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { WORKER_URL, TEST_DOMAIN, createTestAddress } from '../../fixtures/test-helpers'; + +// Regression tests for #956: long admin search queries must not trigger +// D1's "LIKE or GLOB pattern too complex" error. +test.describe('Admin Address Query (#956)', () => { + test('short query (subdomain fragment) returns matching address via LIKE', async ({ request }) => { + const created = await createTestAddress(request, 'q956short'); + const fragment = created.address.split('@')[0].slice(0, 8); + + const res = await request.get(`${WORKER_URL}/admin/address`, { + params: { limit: '20', offset: '0', query: fragment }, + }); + expect(res.ok()).toBe(true); + const body = await res.json(); + expect(Array.isArray(body.results)).toBe(true); + const names: string[] = body.results.map((r: any) => r.name); + expect(names).toContain(created.address); + }); + + test('long query (>50-byte pattern) does not crash with D1 LIKE error', async ({ request }) => { + const longQuery = 'a48r893s@5hx7zb.nationalgeographic.algomindtrade.com'; + expect(new TextEncoder().encode(`%${longQuery}%`).length).toBeGreaterThan(50); + + const res = await request.get(`${WORKER_URL}/admin/address`, { + params: { limit: '20', offset: '0', query: longQuery }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(Array.isArray(body.results)).toBe(true); + expect(body.results.length).toBe(0); + expect(body.count).toBe(0); + }); + + test('long query also works for /admin/users', async ({ request }) => { + const longQuery = 'no-such-user-' + 'x'.repeat(40) + `@${TEST_DOMAIN}`; + expect(new TextEncoder().encode(`%${longQuery}%`).length).toBeGreaterThan(50); + + const res = await request.get(`${WORKER_URL}/admin/users`, { + params: { limit: '20', offset: '0', query: longQuery }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(Array.isArray(body.results)).toBe(true); + expect(body.results.length).toBe(0); + expect(body.count).toBe(0); + }); +}); diff --git a/worker/src/admin_api/admin_user_api.ts b/worker/src/admin_api/admin_user_api.ts index 617daf7..c657c82 100644 --- a/worker/src/admin_api/admin_user_api.ts +++ b/worker/src/admin_api/admin_user_api.ts @@ -39,15 +39,21 @@ export default { getUsers: async (c: Context) => { const { limit, offset, query } = c.req.query(); if (query) { + // D1 caps LIKE pattern length at 50 bytes; fall back to instr() + // for longer queries to avoid "LIKE or GLOB pattern too complex" (#956). + const useInstr = new TextEncoder().encode(query).length + 2 > 50; + const param = useInstr ? query : `%${query}%`; + const userEmailWhere = useInstr ? `instr(u.user_email, ?) > 0` : `u.user_email like ?`; + const userEmailWhereCount = useInstr ? `instr(user_email, ?) > 0` : `user_email like ?`; return await handleListQuery(c, `SELECT u.id as id, u.user_email, u.created_at, u.updated_at,` + ` ur.role_text as role_text,` + ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count` + ` FROM users u` + ` LEFT JOIN user_roles ur ON u.id = ur.user_id` - + ` where u.user_email like ?`, - `SELECT count(*) as count FROM users where user_email like ?`, - [`%${query}%`], limit, offset + + ` where ${userEmailWhere}`, + `SELECT count(*) as count FROM users where ${userEmailWhereCount}`, + [param], limit, offset ); } return await handleListQuery(c, diff --git a/worker/src/admin_api/index.ts b/worker/src/admin_api/index.ts index a84ea9f..e02badf 100644 --- a/worker/src/admin_api/index.ts +++ b/worker/src/admin_api/index.ts @@ -76,14 +76,19 @@ api.get('/admin/address', async (c) => { const sortDirection = sort_order === 'ascend' ? 'asc' : 'desc'; const orderBy = `${sortColumn} ${sortDirection}`; if (query) { + // D1 caps LIKE pattern length at 50 bytes; fall back to instr() for + // longer queries to avoid "LIKE or GLOB pattern too complex" (#956). + const useInstr = new TextEncoder().encode(query).length + 2 > 50; + const whereClause = useInstr ? `instr(name, ?) > 0` : `name like ?`; + const param = useInstr ? query : `%${query}%`; 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` + ` FROM address a` - + ` where name like ?`, - `SELECT count(*) as count FROM address where name like ?`, - [`%${query}%`], limit, offset, orderBy + + ` where ${whereClause}`, + `SELECT count(*) as count FROM address where ${whereClause}`, + [param], limit, offset, orderBy ); } return await handleListQuery(c,