forked from wangziqi/gongxue-base
251 lines
8.8 KiB
TypeScript
251 lines
8.8 KiB
TypeScript
import { HttpError, type RequestContext } from '../../core/http.js';
|
|
import { intParam, stringParam, tenantIdFrom } from '../../core/request.js';
|
|
import { query, queryOne } from '../../core/db.js';
|
|
|
|
function optionalUuidParam(ctx: RequestContext, name: string) {
|
|
const value = stringParam(ctx, name);
|
|
return value || null;
|
|
}
|
|
|
|
export async function contentEntriesRoute(ctx: RequestContext) {
|
|
const tenantId = await tenantIdFrom(ctx);
|
|
const regionId = optionalUuidParam(ctx, 'regionId');
|
|
const entryType = stringParam(ctx, 'entryType');
|
|
const includeHidden = stringParam(ctx, 'includeHidden') === 'true';
|
|
|
|
const params: unknown[] = [tenantId];
|
|
const filters = ['tenant_id = $1', 'is_active = true'];
|
|
if (!includeHidden) filters.push(`visibility <> 'hidden'`);
|
|
if (regionId) {
|
|
params.push(regionId);
|
|
filters.push(`(region_id = $${params.length} or region_id is null)`);
|
|
}
|
|
if (entryType) {
|
|
params.push(entryType);
|
|
filters.push(`entry_type = $${params.length}`);
|
|
}
|
|
|
|
const items = await query(
|
|
`
|
|
select id, region_id as "regionId", legacy_id as "legacyId",
|
|
entry_key as "entryKey", name, entry_type as "entryType",
|
|
icon, route, description, visibility, access_rules as "accessRules",
|
|
layout_config as "layoutConfig", sort_order as "order",
|
|
is_active as "isActive", created_at as "createdAt",
|
|
updated_at as "updatedAt"
|
|
from public.content_entries
|
|
where ${filters.join(' and ')}
|
|
order by sort_order asc, created_at asc
|
|
`,
|
|
params,
|
|
);
|
|
|
|
return { items };
|
|
}
|
|
|
|
export async function contentNodesRoute(ctx: RequestContext) {
|
|
const tenantId = await tenantIdFrom(ctx);
|
|
const entryId = stringParam(ctx, 'entryId');
|
|
if (!entryId) {
|
|
throw new HttpError(400, 'entryId is required', 'REQUIRED_FIELD');
|
|
}
|
|
|
|
const parentIdParam = ctx.url.searchParams.get('parentId');
|
|
const parentId = parentIdParam === 'root' ? null : parentIdParam;
|
|
const mode = stringParam(ctx, 'mode') || 'children';
|
|
const includeInactive = stringParam(ctx, 'includeInactive') === 'true';
|
|
const markerType = stringParam(ctx, 'markerType');
|
|
|
|
const params: unknown[] = [tenantId, entryId];
|
|
const filters = ['tenant_id = $1', 'entry_id = $2'];
|
|
if (!includeInactive) filters.push('is_active = true');
|
|
if (parentIdParam !== null) {
|
|
if (parentId) {
|
|
params.push(parentId);
|
|
filters.push(`parent_id = $${params.length}`);
|
|
} else {
|
|
filters.push('parent_id is null');
|
|
}
|
|
}
|
|
if (markerType) {
|
|
params.push(markerType);
|
|
filters.push(`marker_type = $${params.length}`);
|
|
}
|
|
|
|
const orderClause = mode === 'flat'
|
|
? 'coalesce(path::text, name) asc, sort_order asc'
|
|
: 'sort_order asc, created_at asc';
|
|
|
|
const items = await query(
|
|
`
|
|
select id, entry_id as "entryId", region_id as "regionId",
|
|
parent_id as "parentId", legacy_id as "legacyId",
|
|
node_key as "nodeKey", name, node_type as "nodeType",
|
|
marker_type as "markerType", marker_config as "markerConfig",
|
|
path::text as path, depth, sort_order as "order",
|
|
is_active as "isActive", is_selectable as "isSelectable",
|
|
is_leaf as "isLeaf", access_rules as "accessRules", metadata,
|
|
created_at as "createdAt", updated_at as "updatedAt"
|
|
from public.content_nodes
|
|
where ${filters.join(' and ')}
|
|
order by ${orderClause}
|
|
`,
|
|
params,
|
|
);
|
|
|
|
return { items };
|
|
}
|
|
|
|
export async function questionCollectionsRoute(ctx: RequestContext) {
|
|
const tenantId = await tenantIdFrom(ctx);
|
|
const regionId = optionalUuidParam(ctx, 'regionId');
|
|
const entryId = optionalUuidParam(ctx, 'entryId');
|
|
const nodeId = optionalUuidParam(ctx, 'nodeId');
|
|
const collectionType = stringParam(ctx, 'collectionType');
|
|
const limit = intParam(ctx, 'limit', 100, 500);
|
|
|
|
const params: unknown[] = [tenantId];
|
|
const filters = ['tenant_id = $1', `status = 'active'`];
|
|
if (regionId) {
|
|
params.push(regionId);
|
|
filters.push(`(region_id = $${params.length} or region_id is null)`);
|
|
}
|
|
if (entryId) {
|
|
params.push(entryId);
|
|
filters.push(`entry_id = $${params.length}`);
|
|
}
|
|
if (nodeId) {
|
|
params.push(nodeId);
|
|
filters.push(`node_id = $${params.length}`);
|
|
}
|
|
if (collectionType) {
|
|
params.push(collectionType);
|
|
filters.push(`collection_type = $${params.length}`);
|
|
}
|
|
params.push(limit);
|
|
|
|
const items = await query(
|
|
`
|
|
select id, region_id as "regionId", entry_id as "entryId",
|
|
node_id as "nodeId", subject_id as "subjectId",
|
|
category_id as "categoryId", question_bank_id as "questionBankId",
|
|
legacy_id as "legacyId", name, collection_type as "collectionType",
|
|
source_type as "sourceType", filters, question_count as "questionCount",
|
|
total_score as "totalScore", duration_minutes as "durationMinutes",
|
|
status, sort_order as "order", access_rules as "accessRules", metadata,
|
|
created_at as "createdAt", updated_at as "updatedAt"
|
|
from public.question_collections
|
|
where ${filters.join(' and ')}
|
|
order by sort_order asc, created_at asc
|
|
limit $${params.length}
|
|
`,
|
|
params,
|
|
);
|
|
|
|
return { items };
|
|
}
|
|
|
|
export async function practiceBlueprintsRoute(ctx: RequestContext) {
|
|
const tenantId = await tenantIdFrom(ctx);
|
|
const entryId = optionalUuidParam(ctx, 'entryId');
|
|
const nodeId = optionalUuidParam(ctx, 'nodeId');
|
|
const collectionId = optionalUuidParam(ctx, 'collectionId');
|
|
const mode = stringParam(ctx, 'mode');
|
|
const limit = intParam(ctx, 'limit', 100, 500);
|
|
|
|
const params: unknown[] = [tenantId];
|
|
const filters = ['tenant_id = $1', `status = 'active'`];
|
|
if (entryId) {
|
|
params.push(entryId);
|
|
filters.push(`entry_id = $${params.length}`);
|
|
}
|
|
if (nodeId) {
|
|
params.push(nodeId);
|
|
filters.push(`node_id = $${params.length}`);
|
|
}
|
|
if (collectionId) {
|
|
params.push(collectionId);
|
|
filters.push(`collection_id = $${params.length}`);
|
|
}
|
|
if (mode) {
|
|
params.push(mode);
|
|
filters.push(`mode = $${params.length}`);
|
|
}
|
|
params.push(limit);
|
|
|
|
const items = await query(
|
|
`
|
|
select id, region_id as "regionId", entry_id as "entryId",
|
|
node_id as "nodeId", collection_id as "collectionId",
|
|
legacy_id as "legacyId", name, mode,
|
|
assembly_type as "assemblyType", question_limit as "questionLimit",
|
|
duration_minutes as "durationMinutes", total_score as "totalScore",
|
|
pass_score as "passScore", sections, rules, access_rules as "accessRules", status,
|
|
sort_order as "order", created_at as "createdAt",
|
|
updated_at as "updatedAt"
|
|
from public.practice_blueprints
|
|
where ${filters.join(' and ')}
|
|
order by sort_order asc, created_at asc
|
|
limit $${params.length}
|
|
`,
|
|
params,
|
|
);
|
|
|
|
return { items };
|
|
}
|
|
|
|
export async function collectionQuestionsRoute(ctx: RequestContext) {
|
|
const tenantId = await tenantIdFrom(ctx);
|
|
const collectionId = stringParam(ctx, 'collectionId');
|
|
if (!collectionId) {
|
|
throw new HttpError(400, 'collectionId is required', 'REQUIRED_FIELD');
|
|
}
|
|
const limit = intParam(ctx, 'limit', 200, 1000);
|
|
|
|
const collection = await queryOne<{ id: string }>(
|
|
`
|
|
select id
|
|
from public.question_collections
|
|
where tenant_id = $1 and id = $2 and status = 'active'
|
|
limit 1
|
|
`,
|
|
[tenantId, collectionId],
|
|
);
|
|
if (!collection) {
|
|
throw new HttpError(404, 'Question collection not found', 'QUESTION_COLLECTION_NOT_FOUND');
|
|
}
|
|
|
|
const items = await query(
|
|
`
|
|
select q.id, q.legacy_id as "legacyId", q.entry_id as "entryId",
|
|
q.content_node_id as "contentNodeId",
|
|
q.primary_collection_id as "primaryCollectionId",
|
|
q.subject_id as "subjectId", q.category_id as "categoryId",
|
|
q.node_id as "nodeId", q.type, q.type_label as "typeLabel",
|
|
q.difficulty, q.tags, q.exam_markers as "examMarkers",
|
|
q.media_url as "mediaUrl", q.has_video_explanation as "hasVideoExplanation",
|
|
ci.section_key as "sectionKey", ci.score, ci.sort_order as "order",
|
|
v.id as "versionId", v.content, v.options,
|
|
v.correct_option_index as "correctOptionIndex",
|
|
v.correct_option_indices as "correctOptionIndices",
|
|
v.answer_text as "answerText", v.explanation, v.sub_questions as "subQuestions",
|
|
v.code_lang as "codeLang", v.code_template as "codeTemplate",
|
|
q.created_at as "createdAt", q.updated_at as "updatedAt"
|
|
from public.question_collection_items ci
|
|
join public.questions q on q.id = ci.question_id and q.tenant_id = ci.tenant_id
|
|
left join public.question_versions v
|
|
on v.tenant_id = q.tenant_id
|
|
and v.question_id = q.id
|
|
and v.id = q.current_version_id
|
|
where ci.tenant_id = $1
|
|
and ci.collection_id = $2
|
|
and q.status = 'published'
|
|
order by ci.section_key asc nulls first, ci.sort_order asc, q.created_at asc
|
|
limit $3
|
|
`,
|
|
[tenantId, collectionId, limit],
|
|
);
|
|
|
|
return { items };
|
|
}
|