forked from wangziqi/gongxue-base
feat: add import job detail polling
This commit is contained in:
@@ -3662,6 +3662,27 @@ export async function importVideosRoute(ctx: RequestContext) {
|
||||
);
|
||||
}
|
||||
|
||||
const importJobSelect = `
|
||||
id, import_type as "importType", source_format as "sourceFormat",
|
||||
status, source_name as "sourceName", source_hash as "sourceHash",
|
||||
target_region_id as "targetRegionId", target_subject_id as "targetSubjectId",
|
||||
target_category_id as "targetCategoryId", target_node_id as "targetNodeId",
|
||||
target_question_bank_id as "targetQuestionBankId",
|
||||
target_entry_id as "targetEntryId", target_content_node_id as "targetContentNodeId",
|
||||
target_collection_id as "targetCollectionId",
|
||||
dry_run as "dryRun", total_count as "totalCount",
|
||||
valid_count as "validCount", error_count as "errorCount",
|
||||
warning_count as "warningCount", inserted_count as "insertedCount",
|
||||
updated_count as "updatedCount", skipped_count as "skippedCount",
|
||||
execution_mode as "executionMode", queued_at as "queuedAt",
|
||||
locked_at as "lockedAt", locked_by as "lockedBy",
|
||||
attempt_count as "attemptCount", max_attempts as "maxAttempts",
|
||||
next_attempt_at as "nextAttemptAt", parser_metadata as "parserMetadata",
|
||||
summary, error_message as "errorMessage",
|
||||
started_at as "startedAt", finished_at as "finishedAt",
|
||||
created_by as "createdBy", created_at as "createdAt", updated_at as "updatedAt"
|
||||
`;
|
||||
|
||||
export async function importJobsRoute(ctx: RequestContext) {
|
||||
const { intParam, requireTenantContentEditor, stringParam } = await routeDeps();
|
||||
const auth = await requireTenantContentEditor(ctx);
|
||||
@@ -3683,19 +3704,7 @@ export async function importJobsRoute(ctx: RequestContext) {
|
||||
|
||||
const items = await query(
|
||||
`
|
||||
select id, import_type as "importType", source_format as "sourceFormat",
|
||||
status, source_name as "sourceName", source_hash as "sourceHash",
|
||||
target_region_id as "targetRegionId", target_subject_id as "targetSubjectId",
|
||||
target_category_id as "targetCategoryId", target_node_id as "targetNodeId",
|
||||
target_question_bank_id as "targetQuestionBankId",
|
||||
dry_run as "dryRun", total_count as "totalCount",
|
||||
valid_count as "validCount", error_count as "errorCount",
|
||||
warning_count as "warningCount", inserted_count as "insertedCount",
|
||||
updated_count as "updatedCount", skipped_count as "skippedCount",
|
||||
execution_mode as "executionMode", parser_metadata as "parserMetadata",
|
||||
summary, error_message as "errorMessage",
|
||||
started_at as "startedAt", finished_at as "finishedAt",
|
||||
created_by as "createdBy", created_at as "createdAt", updated_at as "updatedAt"
|
||||
select ${importJobSelect}
|
||||
from public.content_import_jobs
|
||||
where ${filters.join(' and ')}
|
||||
order by created_at desc
|
||||
@@ -3707,6 +3716,80 @@ export async function importJobsRoute(ctx: RequestContext) {
|
||||
return { items };
|
||||
}
|
||||
|
||||
export async function importJobDetailRoute(ctx: RequestContext) {
|
||||
const { intParam, requireTenantContentEditor, requiredString, stringParam } = await routeDeps();
|
||||
const auth = await requireTenantContentEditor(ctx);
|
||||
const jobId = requiredString({ jobId: stringParam(ctx, 'jobId') }, 'jobId');
|
||||
const issueLimit = intParam(ctx, 'issueLimit', 20, 200);
|
||||
|
||||
const item = await queryOne<Record<string, unknown>>(
|
||||
`
|
||||
select ${importJobSelect}
|
||||
from public.content_import_jobs
|
||||
where tenant_id = $1 and id = $2
|
||||
limit 1
|
||||
`,
|
||||
[auth.tenantId, jobId],
|
||||
);
|
||||
if (!item) {
|
||||
throw new HttpError(404, 'Import job not found', 'IMPORT_JOB_NOT_FOUND');
|
||||
}
|
||||
|
||||
const issueSummary = await query<{ severity: string; count: string }>(
|
||||
`
|
||||
select severity, count(*)::text as count
|
||||
from public.content_import_issues
|
||||
where tenant_id = $1 and job_id = $2
|
||||
group by severity
|
||||
order by case severity when 'error' then 1 when 'warning' then 2 else 9 end
|
||||
`,
|
||||
[auth.tenantId, jobId],
|
||||
);
|
||||
const itemStatusSummary = await query<{ status: string; count: string }>(
|
||||
`
|
||||
select status, count(*)::text as count
|
||||
from public.content_import_items
|
||||
where tenant_id = $1 and job_id = $2
|
||||
group by status
|
||||
order by status asc
|
||||
`,
|
||||
[auth.tenantId, jobId],
|
||||
);
|
||||
const recentIssues = await query(
|
||||
`
|
||||
select i.id, i.row_no as "rowNo", i.severity, i.code,
|
||||
i.field_path as "fieldPath", i.message, i.details,
|
||||
item.external_id as "externalId", item.status as "itemStatus",
|
||||
i.created_at as "createdAt"
|
||||
from public.content_import_issues i
|
||||
left join public.content_import_items item on item.id = i.item_id
|
||||
where i.tenant_id = $1 and i.job_id = $2
|
||||
order by i.row_no asc nulls last, case i.severity when 'error' then 1 else 2 end, i.created_at asc
|
||||
limit $3
|
||||
`,
|
||||
[auth.tenantId, jobId, issueLimit],
|
||||
);
|
||||
|
||||
const summary = objectValue(item.summary);
|
||||
return {
|
||||
item,
|
||||
issueSummary: Object.fromEntries(issueSummary.map(row => [row.severity, Number(row.count || 0)])),
|
||||
itemStatusSummary: Object.fromEntries(itemStatusSummary.map(row => [row.status, Number(row.count || 0)])),
|
||||
recentIssues,
|
||||
importPostCheck: summary.importPostCheck || null,
|
||||
worker: {
|
||||
executionMode: item.executionMode,
|
||||
queuedAt: item.queuedAt,
|
||||
lockedAt: item.lockedAt,
|
||||
lockedBy: item.lockedBy,
|
||||
attemptCount: item.attemptCount,
|
||||
maxAttempts: item.maxAttempts,
|
||||
nextAttemptAt: item.nextAttemptAt,
|
||||
lastWorkerError: objectValue(summary.lastWorkerError),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function importIssuesRoute(ctx: RequestContext) {
|
||||
const { intParam, requireTenantContentEditor, requiredString, stringParam } = await routeDeps();
|
||||
const auth = await requireTenantContentEditor(ctx);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from './assets.js';
|
||||
import {
|
||||
importHandbookRoute,
|
||||
importJobDetailRoute,
|
||||
importIssuesRoute,
|
||||
importJobsRoute,
|
||||
importQuestionsRoute,
|
||||
@@ -111,6 +112,7 @@ export const tenantContentRoutes: RouteDefinition[] = [
|
||||
['POST', '/api/tenant-content/imports/preview/videos', previewVideosImportRoute],
|
||||
['POST', '/api/tenant-content/imports/videos', importVideosRoute],
|
||||
['GET', '/api/tenant-content/imports', importJobsRoute],
|
||||
['GET', '/api/tenant-content/imports/detail', importJobDetailRoute],
|
||||
['GET', '/api/tenant-content/imports/issues', importIssuesRoute],
|
||||
['GET', '/api/tenant-content/imports/field-mapping', importFieldMappingRoute],
|
||||
['GET', '/api/tenant-content/imports/templates', importTemplateRoute],
|
||||
|
||||
Reference in New Issue
Block a user