fix(projects): находить проекты с кириллицей без учёта регистра

SQLite LOWER() и LIKE понижают только ASCII, поэтому «Сайт и API» не
находился и каждый новый VPS с таким проектом создавал дубль; подсказки
по «сай» тоже были пустыми. Сравнение перенесено в JS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-06 22:58:30 +07:00
1 parent 5e29ff7ec3
commit d0e262c41d
2 files changed
+27 -23

No files matched your search

+12
View File
@@ -30,6 +30,18 @@ describe('projects routes', () => {
expect(body[0]?.color).toBe('#ff0000')
})
it('reuses a Cyrillic project regardless of case and suggests it', async () => {
projectsRepository.create({ name: 'Сайт и API' })
for (const project of ['Сайт и API', 'сайт и api']) {
const res = await app.inject({ method: 'POST', url: '/api/vps', payload: { ip: '10.0.0.1', project } })
expect(res.statusCode).toBe(201)
}
const list = (await app.inject({ url: '/api/projects' })).json() as { name: string }[]
expect(list.map((p) => p.name)).toEqual(['Сайт и API'])
const sug = (await app.inject({ url: `/api/projects/suggest?q=${encodeURIComponent('сай')}` })).json() as { name: string }[]
expect(sug.map((p) => p.name)).toEqual(['Сайт и API'])
})
it('creates project with color and notes', async () => {
const res = await app.inject({
method: 'POST',
+15 -23
View File
@@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto'
import { and, eq, like, asc, sql } from 'drizzle-orm'
import { and, eq, asc, sql } from 'drizzle-orm'
import { getDb, schema } from '../index.js'
import { getCurrentSpaceId } from '../space-context.js'
@@ -13,17 +13,18 @@ export function findProjectByNameCaseInsensitive(
): (typeof schema.serverProjects.$inferSelect) | undefined {
const n = normalizeProjectNameInput(name)
if (!n) return undefined
const spaceId = getCurrentSpaceId()
// SQLite LOWER()/LIKE fold ASCII only — «Сайт» never matched «сайт», so each VPS created a duplicate project.
const key = n.toLowerCase()
return listSpaceProjects().find((p) => p.name.trim().toLowerCase() === key)
}
function listSpaceProjects() {
return getDb()
.select()
.from(schema.serverProjects)
.where(
and(
eq(schema.serverProjects.spaceId, spaceId),
eq(sql`LOWER(${schema.serverProjects.name})`, n.toLowerCase()),
),
)
.get()
.where(eq(schema.serverProjects.spaceId, getCurrentSpaceId()))
.orderBy(asc(schema.serverProjects.name))
.all()
}
export function resolveOrCreateProject(
@@ -67,20 +68,11 @@ export function projectSuggestions(
.limit(lim)
.all()
}
const esc = term.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
const pattern = `%${esc.toLowerCase()}%`
return db
.select({ id: schema.serverProjects.id, name: schema.serverProjects.name })
.from(schema.serverProjects)
.where(
and(
eq(schema.serverProjects.spaceId, spaceId),
like(sql`LOWER(${schema.serverProjects.name})`, pattern),
),
)
.orderBy(asc(schema.serverProjects.name))
.limit(lim)
.all()
const needle = term.toLowerCase()
return listSpaceProjects()
.filter((p) => p.name.toLowerCase().includes(needle))
.slice(0, lim)
.map(({ id, name }) => ({ id, name }))
}
export function getProjectNameById(id: string): string {