feat: build legacy practice navigation

This commit is contained in:
Codex
2026-06-30 12:47:27 +08:00
parent fce5513464
commit d005f65fe7
12 changed files with 2460 additions and 11 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,232 @@ async function validationChecks(): Promise<CheckResult[]> {
),
);
checks.push(
result(
'legacy_practice_navigation_missing_entries',
await scalar(
`
select case
when exists (
select 1 from public.questions
where tenant_id = $1 and legacy_id is not null
)
and not exists (
select 1 from public.content_entries
where tenant_id = $1
and entry_type = 'question_practice'
and legacy_id is not null
)
then 1 else 0 end
`,
[tenantId],
),
'Imported legacy questions exist but no question_practice content entries were generated.',
'Legacy question practice content entries are present when imported questions exist.',
),
);
checks.push(
result(
'legacy_practice_navigation_missing_nodes',
await scalar(
`
select case
when exists (
select 1 from public.questions
where tenant_id = $1 and legacy_id is not null
)
and not exists (
select 1 from public.content_nodes
where tenant_id = $1
and legacy_id is not null
and metadata->>'source' in (
'pocketbase.module_nodes',
'pocketbase.subjects',
'pocketbase.categories',
'pocketbase.migration_review.unresolved_category'
)
)
then 1 else 0 end
`,
[tenantId],
),
'Imported legacy questions exist but no content nodes were generated.',
'Legacy content nodes are present when imported questions exist.',
),
);
checks.push(
result(
'legacy_practice_collections_missing_items',
await scalar(
`
select count(*)
from public.question_collections qc
where qc.tenant_id = $1
and qc.legacy_id is not null
and qc.status = 'active'
and qc.question_count = 0
and exists (
select 1
from public.questions q
left join public.content_nodes cn
on cn.tenant_id = q.tenant_id
and cn.id = q.content_node_id
where q.tenant_id = qc.tenant_id
and q.status = 'published'
and (
q.content_node_id = qc.node_id
or q.category_id = qc.category_id
or (qc.category_id is null and q.subject_id = qc.subject_id)
)
)
`,
[tenantId],
),
'Active legacy question collections exist without collection items even though matching questions exist.',
'Active legacy question collections have item counts when matching questions exist.',
),
);
checks.push(
result(
'legacy_questions_without_navigation',
await scalar(
`
select count(*)
from public.questions
where tenant_id = $1
and legacy_id is not null
and status = 'published'
and (
entry_id is null
or content_node_id is null
or primary_collection_id is null
)
`,
[tenantId],
),
'Some imported published questions are not attached to entry/node/collection navigation.',
'Imported published questions are attached to entry/node/collection navigation.',
),
);
checks.push(
result(
'legacy_unresolved_categories_not_isolated',
await scalar(
`
select count(*)
from public.questions q
left join public.question_collections qc
on qc.tenant_id = q.tenant_id
and qc.id = q.primary_collection_id
left join public.content_nodes cn
on cn.tenant_id = q.tenant_id
and cn.id = q.content_node_id
where q.tenant_id = $1
and q.legacy_id is not null
and q.legacy_category_id is not null
and q.category_id is null
and q.node_id is null
and (
qc.id is null
or qc.status <> 'draft'
or qc.access_rules->>'requiresReview' <> 'true'
or cn.id is null
or cn.is_active <> false
or cn.access_rules->>'requiresReview' <> 'true'
)
`,
[tenantId],
),
'Questions with unresolved legacy categories are not isolated into draft review collections/inactive nodes.',
'Questions with unresolved legacy categories are isolated for tenant-admin review.',
),
);
checks.push(
result(
'legacy_orphan_node_questions_not_isolated',
await scalar(
`
select count(*)
from public.questions q
left join public.question_collections qc
on qc.tenant_id = q.tenant_id
and qc.id = q.primary_collection_id
left join public.content_nodes cn
on cn.tenant_id = q.tenant_id
and cn.id = q.content_node_id
left join public.content_entries ce
on ce.tenant_id = q.tenant_id
and ce.id = q.entry_id
where q.tenant_id = $1
and q.legacy_id is not null
and q.legacy_node_id is not null
and q.node_id is null
and q.category_id is null
and q.legacy_category_id is null
and not (
qc.status = 'draft'
and qc.access_rules->>'requiresReview' = 'true'
and qc.metadata->>'source' = 'pocketbase.migration_review.orphan_subject'
and cn.is_active = false
and cn.access_rules->>'requiresReview' = 'true'
and ce.visibility = 'hidden'
and ce.access_rules->>'requiresReview' = 'true'
)
and not (
qc.status = 'active'
and qc.metadata->>'source' = 'pocketbase.subjects'
and qc.metadata->>'scope' = 'subject_all'
and cn.node_type = 'subject'
and cn.marker_type = 'subject'
and cn.is_active = true
and ce.visibility <> 'hidden'
)
`,
[tenantId],
),
'Questions with unresolved legacy nodeId are neither isolated into hidden review navigation nor attached to a public subject fallback.',
'Questions with unresolved legacy nodeId are isolated or attached to a public subject fallback.',
),
);
checks.push(
result(
'legacy_node_questions_wrong_navigation',
await scalar(
`
select count(*)
from public.questions q
join public.module_nodes mn
on mn.tenant_id = q.tenant_id
and mn.id = q.node_id
left join public.content_nodes cn
on cn.tenant_id = q.tenant_id
and cn.legacy_id = 'module_node:' || mn.legacy_id
left join public.question_collections qc
on qc.tenant_id = q.tenant_id
and qc.legacy_id = 'module_node:' || mn.legacy_id || ':direct'
where q.tenant_id = $1
and q.legacy_id is not null
and q.node_id is not null
and (
cn.id is null
or q.content_node_id is distinct from cn.id
or qc.id is null
or q.primary_collection_id is distinct from qc.id
)
`,
[tenantId],
),
'Some imported questions with legacy nodeId are not attached to the matching content node/direct collection.',
'Imported questions with legacy nodeId are attached to matching content nodes and direct collections.',
),
);
checks.push(
result(
'legacy_mock_blueprints_incomplete',