このコミットが含まれているのは:
2026-07-16 18:52:07 +09:00
コミット 95c3f08a44
11個のファイルの変更317行の追加203行の削除
+56 -4
ファイルの表示
@@ -3,8 +3,10 @@ import { describe, expect, it } from 'vitest'
import { creatableImportRows,
initialisePreviewRows,
mergeImportResults,
mergeValidatedImportRow,
mergeValidatedImportRows,
processableImportRows,
replaceImportRow,
resultRepairMode,
resultRowMessages,
resultSummaryCounts,
@@ -84,13 +86,16 @@ describe ('post import row state', () => {
it ('merges result states and clears incompatible post identifiers', () => {
const created = mergeImportResults ([buildPostImportRow ({
skipReason: 'existing',
existingPostId: 2 })], [{
existingPostId: 2,
recoverable: true,
importStatus: 'pending' })], [{
sourceRow: 1,
status: 'created',
post: { id: 3 } }])[0]
const skipped = mergeImportResults ([buildPostImportRow ({
createdPostId: 3,
importStatus: 'created' })], [{
importStatus: 'created',
recoverable: true })], [{
sourceRow: 1,
status: 'skipped',
existingPostId: 4 }])[0]
@@ -99,20 +104,24 @@ describe ('post import row state', () => {
existingPostId: 4 })], [{
sourceRow: 1,
status: 'failed',
recoverable: true,
errors: { base: ['failure'] } }])[0]
expect (created).toMatchObject ({
importStatus: 'created',
createdPostId: 3,
existingPostId: undefined,
recoverable: undefined,
skipReason: undefined })
expect (skipped).toMatchObject ({
importStatus: 'skipped',
existingPostId: 4,
createdPostId: undefined,
recoverable: undefined,
skipReason: 'existing' })
expect (failed).toMatchObject ({
importStatus: 'failed',
recoverable: true,
createdPostId: undefined,
existingPostId: undefined,
skipReason: undefined,
@@ -125,12 +134,15 @@ describe ('post import row state', () => {
buildPostImportRow ({ sourceRow: 2, importStatus: 'skipped',
existingPostId: 2, skipReason: 'existing' }),
buildPostImportRow ({ sourceRow: 3, importStatus: 'failed',
importErrors: { base: ['failed'] } })]
recoverable: true, importErrors: { base: ['failed'] } }),
buildPostImportRow ({ sourceRow: 4, importStatus: 'pending',
recoverable: true, validationErrors: { base: ['failed'] } })]
expect (resultSummaryCounts (rows)).toEqual ({ created: 1, skipped: 1, failed: 1 })
expect (resultSummaryCounts (rows)).toEqual ({ created: 1, skipped: 1, failed: 2 })
expect (retryImportRow (rows, 3)[2]).toMatchObject ({
importStatus: 'pending',
importErrors: undefined })
expect (retryImportRow (rows, 4)[3]).toBe (rows[3])
})
it ('deduplicates messages and keeps repair mode only for repairable rows', () => {
@@ -149,6 +161,46 @@ describe ('post import row state', () => {
expect (resultRepairMode ([complete])).toBe ('all')
})
it ('merges only the validated source row and preserves other row edits', () => {
const edited = buildPostImportRow ({
sourceRow: 1,
attributes: { title: 'edited row' },
provenance: { title: 'manual' } })
const other = buildPostImportRow ({
sourceRow: 2,
attributes: { title: 'keep me' },
provenance: { title: 'manual' } })
const validated = buildPostImportRow ({
sourceRow: 1,
attributes: { title: 'validated row' } })
const result = mergeValidatedImportRow ([edited, other], validated)
expect (result[0]?.attributes.title).toBe ('validated row')
expect (result[1]?.attributes.title).toBe ('keep me')
})
it ('replaces only the targeted source row and preserves the others', () => {
const original = buildPostImportRow ({
sourceRow: 1,
importStatus: 'failed',
recoverable: true,
importErrors: { base: ['failed'] } })
const other = buildPostImportRow ({
sourceRow: 2,
attributes: { title: 'keep edited row' } })
const restored = buildPostImportRow ({
sourceRow: 1,
importStatus: 'failed',
recoverable: true,
importErrors: { base: ['failed'] } })
const result = replaceImportRow ([original, other], restored)
expect (result[0]).toEqual (restored)
expect (result[1]?.attributes.title).toBe ('keep edited row')
})
it ('copies reset snapshot values instead of sharing mutable records', () => {
const row = buildPostImportRow ({ fieldWarnings: { title: ['warning'] } })
const initialised = initialisePreviewRows ([row])[0]
+39 -3
ファイルの表示
@@ -7,8 +7,12 @@ const hasSkipReason = (row: PostImportRow): boolean =>
const hasValidationErrors = (row: PostImportRow): boolean =>
Object.keys (row.validationErrors ?? { }).length > 0
const isRecoverableRow = (row: PostImportRow): boolean =>
row.recoverable === true
const isRepairableImportStatus = (row: PostImportRow): boolean =>
row.importStatus === 'failed' || row.importStatus === 'pending'
isRecoverableRow (row)
&& (row.importStatus === 'failed' || row.importStatus === 'pending')
const buildResetSnapshot = (row: PostImportRow) => ({
url: row.url,
@@ -63,7 +67,8 @@ export const resultSummaryCounts = (rows: PostImportRow[]) =>
++counts.skipped
return counts
}
if (row.importStatus === 'failed')
if (row.importStatus === 'failed'
|| (row.recoverable === true && row.importStatus === 'pending'))
++counts.failed
return counts
},
@@ -84,6 +89,32 @@ export const resultRowMessages = (row: PostImportRow): string[] =>
...Object.values (row.importErrors ?? { }).flat ()])]
export const resultRowWarnings = (row: PostImportRow): string[] =>
[...new Set ([
...Object.values (row.fieldWarnings ?? { }).flat (),
...row.baseWarnings])]
export const replaceImportRow = (
rows: PostImportRow[],
nextRow: PostImportRow,
): PostImportRow[] =>
rows.map (row => row.sourceRow === nextRow.sourceRow ? nextRow : row)
export const mergeValidatedImportRow = (
rows: PostImportRow[],
validated: PostImportRow,
): PostImportRow[] => {
const current = rows.find (row => row.sourceRow === validated.sourceRow)
if (current == null)
return rows
const [merged] = mergeValidatedImportRows ([current], [validated])
return merged == null ? rows : replaceImportRow (rows, merged)
}
export const mergeValidatedImportRows = (
current: PostImportRow[],
validated: PostImportRow[],
@@ -150,6 +181,7 @@ export const mergeImportResults = (
return {
...row,
importStatus: 'created',
recoverable: undefined,
skipReason: undefined,
createdPostId: result.post.id,
existingPostId: undefined,
@@ -160,6 +192,7 @@ export const mergeImportResults = (
return {
...row,
importStatus: 'skipped',
recoverable: undefined,
skipReason: 'existing',
createdPostId: undefined,
existingPostId: result.existingPostId,
@@ -170,6 +203,7 @@ export const mergeImportResults = (
return {
...row,
importStatus: 'failed',
recoverable: result.recoverable === true ? true : undefined,
skipReason: undefined,
createdPostId: undefined,
existingPostId: undefined,
@@ -186,7 +220,9 @@ export const retryImportRow = (
sourceRow: number,
): PostImportRow[] =>
rows.map (row =>
row.sourceRow === sourceRow && row.importStatus === 'failed'
row.sourceRow === sourceRow
&& row.importStatus === 'failed'
&& row.recoverable === true
? { ...row, importStatus: 'pending', importErrors: undefined }
: row)
+14 -4
ファイルの表示
@@ -16,18 +16,25 @@ describe ('post import storage', () => {
it ('round-trips a valid session and source draft', () => {
const row = buildPostImportRow ({
recoverable: true,
importStatus: 'pending',
validationErrors: { title: ['invalid'] } })
const skipped = buildPostImportRow ({
importStatus: 'skipped',
skipReason: 'existing',
existingPostId: 10 })
expect (savePostImportSession ('session', {
source: row.url,
rows: [row],
source: skipped.url,
rows: [row, skipped],
repairMode: 'all' })).toBe (true)
expect (loadPostImportSession ('session')).toMatchObject ({
version: 2,
source: row.url,
source: skipped.url,
rows: [{
recoverable: true,
importStatus: 'pending' },
{
importStatus: 'skipped',
skipReason: 'existing',
existingPostId: 10 }] })
@@ -49,7 +56,10 @@ describe ('post import storage', () => {
{ ...session.rows[0], skipReason: 'existing', existingPostId: undefined },
{ ...session.rows[0], existingPostId: 2, skipReason: undefined },
{ ...session.rows[0], importStatus: 'created', createdPostId: undefined },
{ ...session.rows[0], importStatus: 'failed', createdPostId: 3 }]
{ ...session.rows[0], importStatus: 'failed', createdPostId: 3 },
{ ...session.rows[0], importStatus: 'created', recoverable: true },
{ ...session.rows[0], importStatus: 'skipped', recoverable: true },
{ ...session.rows[0], recoverable: true }]
for (const [index, row] of invalidRows.entries ())
{
+11 -1
ファイルの表示
@@ -202,6 +202,8 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
return null
if (value.skipReason != null && !(isValidSkipReason (value.skipReason)))
return null
if (value.recoverable != null && value.recoverable !== true)
return null
if (value.skipReason === 'existing' && !(isPositiveInteger (value.existingPostId)))
return null
if (value.skipReason !== 'existing' && value.existingPostId != null)
@@ -210,6 +212,13 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
return null
if (value.importStatus !== 'created' && value.createdPostId != null)
return null
if (value.recoverable === true
&& value.importStatus !== 'failed'
&& value.importStatus !== 'pending')
return null
if ((value.importStatus === 'created' || value.importStatus === 'skipped')
&& value.recoverable != null)
return null
const validationErrors = ensureStringListRecord (value.validationErrors)
const fieldWarnings = ensureStringListRecord (value.fieldWarnings)
@@ -268,7 +277,8 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
resetSnapshot,
createdPostId:
isPositiveInteger (value.createdPostId) ? Number (value.createdPostId) : undefined,
importStatus: value.importStatus ?? undefined }
importStatus: value.importStatus ?? undefined,
recoverable: value.recoverable === true ? true : undefined }
}
+2 -1
ファイルの表示
@@ -34,7 +34,8 @@ export type PostImportRow = {
metadataUrl?: string
resetSnapshot: PostImportResetSnapshot
createdPostId?: number
importStatus?: PostImportStatus }
importStatus?: PostImportStatus
recoverable?: boolean }
export type PostImportResultRow =
| {