このコミットが含まれているのは:
2026-11-01 18:18:54 +09:00
コミット 22e57564a5
18個のファイルの変更、313行の追加、121行の削除
+25 -1
ファイルの表示
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { columnsForCounts, countColumns, countsCSV, countsDefinitionParams,
import { columnsForCounts, countColumns, countsCSV, countsDefinitionParams, countsResourcePath,
countsRequestParams, normaliseCountsParams, selectedCountChildren } from '@/lib/counts'
import { countsErrorMessage, countsConditionValue, countsNestedIsScalar, countsNestedScalarText }
from '@/lib/countsDisplay'
@@ -14,6 +14,30 @@ import type { CountsCondition } from '@/lib/countsDefinitions'
describe ('Counts URL contract', () => {
it ('distinguishes HTTP, network and internal failures without server details', () => {
const http = { isAxiosError: true, response: { status: 503, data: '<private SQL>' } }
expect (countsErrorMessage (http, 'ja').text).toContain ('HTTP 503')
expect (countsErrorMessage (http, 'ja').text).not.toContain ('private SQL')
expect (countsErrorMessage ({ isAxiosError: true }, 'ja').text)
.toBe ('APIサーバーとの通信に失敗しました。')
expect (countsErrorMessage (new Error ('private stack'), 'ja').text)
.toBe ('内部処理で問題が発生しました。')
})
it ('explains a totals timeout using the server time limit', () => {
const failure = { isAxiosError: true, response: { status: 503,
data: { error: 'counts_query_timeout', stage: 'totals', timeout_seconds: 30 } } }
const text = countsErrorMessage (failure, 'ja').text
expect (text).toContain ('集計処理が制限時間を超えました。')
expect (text).toContain ('小計・総計の算出が30秒以内に完了しませんでした。')
expect (text).toContain ('集計期間や行・列の項目を減らして')
expect (countsErrorMessage (failure, 'ain-Kana').language).toBe ('ja')
})
it ('uses hyphenated page paths for internal resource identifiers', () => {
expect (countsResourcePath ('nico_tags')).toBe ('/counts/nico-tags')
expect (countsResourcePath ('tag_descriptions')).toBe ('/counts/tag-descriptions')
expect (countsResourcePath ('tag_groups')).toBe ('/counts/tag-groups')
expect (countsResourcePath ('posts')).toBe ('/counts/posts')
})
it ('defaults to latest summary, limit 100, and no axes', () => {
const params = normaliseCountsParams ('posts', new URLSearchParams (), postDefinition)
expect (params.get ('definition')).toBe ('latest')
+4
ファイルの表示
@@ -12,6 +12,10 @@ export const countsMaskedValue = '__counts_private__'
export type CountsResource = 'posts' | 'tags' | 'nico_tags'
| 'tag_descriptions' | 'tag_groups' | 'materials'
export const countsResourcePath = (resource: CountsResource): string =>
`/counts/${resource.replaceAll ('_', '-')}`
export type CountsName = TagNameSnapshot & {
languageCode: string; languageName?: string | null; scriptName?: string | null }
export type CountsTag = {
+13 -3
ファイルの表示
@@ -17,7 +17,8 @@ import type { LocaleCode } from '@/lib/locales'
export const countsErrorMessage = (failure: unknown, locale: LocaleCode) => {
const response = isApiError (failure) ? failure.response : null
const body = (response?.data ?? failure) as {
error?: unknown; max_axes?: unknown; maxAxes?: unknown
error?: unknown; stage?: unknown; timeout_seconds?: unknown; timeoutSeconds?: unknown
max_axes?: unknown; maxAxes?: unknown
max_columns?: unknown; maxColumns?: unknown } | null
const code = body != null && typeof body === 'object' ? body.error : null
const message = (key: string, values?: Record<string, string | number>) =>
@@ -31,6 +32,11 @@ export const countsErrorMessage = (failure: unknown, locale: LocaleCode) => {
max: maximum })
}
if (code === 'private_condition') return message ('privateCondition')
if (code === 'counts_query_timeout' && body?.stage === 'totals')
{
const seconds = body.timeoutSeconds ?? body.timeout_seconds
if (typeof seconds === 'number') return message ('errors.totalsTimeout', { seconds })
}
if (code === 'counts_query_timeout' || code === 'generation_timeout')
return message ('errors.queryTimeout')
if (code === 'export_worker_unavailable' || code === 'worker_unavailable')
@@ -40,7 +46,9 @@ export const countsErrorMessage = (failure: unknown, locale: LocaleCode) => {
if (response?.status === 401) return message ('errors.authentication')
if (response?.status === 403) return message ('errors.permission')
if (response?.status === 400 || response?.status === 422) return message ('errors.input')
return message ('common:fetchError')
if (response != null) return message ('errors.http', { status: response.status })
if (isApiError (failure)) return message ('errors.network')
return message ('errors.internal')
}
@@ -158,7 +166,7 @@ export const countsLogicalName = (
if (value == null) return { text: '' }
if (field.startsWith ('aliases_') && Array.isArray (value))
return { text: value.map (entry => typeof entry === 'string' ? entry
: String (entry?.name ?? '')).join ('\n') }
: String (entry?.name ?? '')).join (' ') }
if (field === 'sections')
return { text: (countsSectionRanges (value, locale) ?? []).join (
translateUIMessage ('sections.separator', 'counts', locale).text) }
@@ -528,6 +536,8 @@ export const countsSummaryValue = (
const logical = data.axisLabels?.[`a${index}`]?.[String (value)]
if (axis.field === 'user') return message ('actorWithId', {
name: logical || message ('notRegistered'), id: value })
if (axis.field === 'parent_tag_ids')
return String (value) === '' ? '' : logical || message ('notRegistered')
if (logical != null) return logical
if (axis.field === 'tags')
return message ('notRegistered')
+2 -2
ファイルの表示
@@ -1,4 +1,4 @@
import { isCountsState, selectedCountFields } from '@/lib/counts'
import { countsResourcePath, isCountsState, selectedCountFields } from '@/lib/counts'
import { translateUIMessage } from '@/lib/i18n'
import type { CountField, CountsResource, CountsResult } from '@/lib/counts'
@@ -90,5 +90,5 @@ export const countsSummaryDrill = (
next.set ('before',
(before != null && Date.parse (before) < Date.parse (last)) ? before : last)
}
return `/counts/${resource}?${next}`
return `${countsResourcePath (resource)}?${next}`
}