forked from wangziqi/gongxue-base
feat: add content navigation practice assembly
This commit is contained in:
@@ -6,29 +6,103 @@ import { boolValue, intValue, jsonArrayValue, jsonObjectValue, nullableString, o
|
||||
|
||||
const QUESTION_STATUSES = ['draft', 'published', 'archived'];
|
||||
|
||||
async function assertOptionalReference(
|
||||
client: { query: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }> },
|
||||
tenantId: string,
|
||||
table: string,
|
||||
id: string | null,
|
||||
code: string,
|
||||
) {
|
||||
if (!id) return;
|
||||
const row = await client.query(`select id from public.${table} where tenant_id = $1 and id = $2 limit 1`, [tenantId, id]);
|
||||
if (!row.rows[0]) {
|
||||
throw new HttpError(400, `${table} reference is not in this tenant`, code);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPrimaryCollectionItem(
|
||||
client: { query: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }> },
|
||||
tenantId: string,
|
||||
collectionId: string | null,
|
||||
questionId: string,
|
||||
) {
|
||||
if (!collectionId) return;
|
||||
await client.query(
|
||||
`
|
||||
insert into public.question_collection_items (tenant_id, collection_id, question_id, sort_order)
|
||||
values (
|
||||
$1, $2, $3,
|
||||
coalesce((select max(sort_order) + 1 from public.question_collection_items where tenant_id = $1 and collection_id = $2), 0)
|
||||
)
|
||||
on conflict (tenant_id, collection_id, question_id) do nothing
|
||||
`,
|
||||
[tenantId, collectionId, questionId],
|
||||
);
|
||||
await client.query(
|
||||
`
|
||||
update public.question_collections
|
||||
set question_count = (
|
||||
select count(*)
|
||||
from public.question_collection_items
|
||||
where tenant_id = $1 and collection_id = $2
|
||||
),
|
||||
updated_at = now()
|
||||
where tenant_id = $1 and id = $2
|
||||
`,
|
||||
[tenantId, collectionId],
|
||||
);
|
||||
}
|
||||
|
||||
export async function createQuestionRoute(ctx: RequestContext) {
|
||||
const auth = await requireTenantContentEditor(ctx);
|
||||
const body = await readJsonBody(ctx);
|
||||
|
||||
const item = await transaction(async client => {
|
||||
const questionBankId = nullableString(body.questionBankId);
|
||||
const subjectId = nullableString(body.subjectId);
|
||||
const categoryId = nullableString(body.categoryId);
|
||||
const nodeId = nullableString(body.nodeId);
|
||||
const entryId = nullableString(body.entryId);
|
||||
const contentNodeId = nullableString(body.contentNodeId);
|
||||
const primaryCollectionId = nullableString(body.primaryCollectionId) || nullableString(body.collectionId);
|
||||
|
||||
await assertOptionalReference(client, auth.tenantId, 'question_banks', questionBankId, 'QUESTION_BANK_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'subjects', subjectId, 'SUBJECT_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'categories', categoryId, 'CATEGORY_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'module_nodes', nodeId, 'NODE_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'content_entries', entryId, 'ENTRY_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'content_nodes', contentNodeId, 'CONTENT_NODE_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'question_collections', primaryCollectionId, 'QUESTION_COLLECTION_NOT_FOUND');
|
||||
|
||||
const questionResult = await client.query(
|
||||
`
|
||||
insert into public.questions (
|
||||
tenant_id, question_bank_id, subject_id, category_id, node_id,
|
||||
legacy_id, type, type_label, difficulty, tags, media_url, status
|
||||
entry_id, content_node_id, primary_collection_id, legacy_id, type,
|
||||
type_label, difficulty, tags, media_url, status, exam_markers
|
||||
)
|
||||
values (
|
||||
$1, $2::uuid, $3::uuid, $4::uuid, $5::uuid,
|
||||
$6::uuid, $7::uuid, $8::uuid, $9, $10,
|
||||
$11, $12, $13::jsonb, $14, $15, $16::jsonb
|
||||
)
|
||||
values ($1, $2::uuid, $3::uuid, $4::uuid, $5::uuid, $6, $7, $8, $9, $10::jsonb, $11, $12)
|
||||
returning id, tenant_id as "tenantId", question_bank_id as "questionBankId",
|
||||
subject_id as "subjectId", category_id as "categoryId", node_id as "nodeId",
|
||||
type, type_label as "typeLabel", difficulty, tags, media_url as "mediaUrl",
|
||||
status, created_at as "createdAt", updated_at as "updatedAt"
|
||||
entry_id as "entryId", content_node_id as "contentNodeId",
|
||||
primary_collection_id as "primaryCollectionId", type,
|
||||
type_label as "typeLabel", difficulty, tags, media_url as "mediaUrl",
|
||||
status, exam_markers as "examMarkers", created_at as "createdAt",
|
||||
updated_at as "updatedAt"
|
||||
`,
|
||||
[
|
||||
auth.tenantId,
|
||||
nullableString(body.questionBankId),
|
||||
nullableString(body.subjectId),
|
||||
nullableString(body.categoryId),
|
||||
nullableString(body.nodeId),
|
||||
questionBankId,
|
||||
subjectId,
|
||||
categoryId,
|
||||
nodeId,
|
||||
entryId,
|
||||
contentNodeId,
|
||||
primaryCollectionId,
|
||||
nullableString(body.legacyId),
|
||||
optionalString(body, 'type') || 'choice',
|
||||
optionalString(body, 'typeLabel') || null,
|
||||
@@ -36,9 +110,11 @@ export async function createQuestionRoute(ctx: RequestContext) {
|
||||
jsonArrayValue(body.tags),
|
||||
nullableString(body.mediaUrl),
|
||||
optionalStatus(body.status, QUESTION_STATUSES, 'published'),
|
||||
jsonObjectValue(body.examMarkers),
|
||||
],
|
||||
);
|
||||
const question = questionResult.rows[0];
|
||||
await syncPrimaryCollectionItem(client, auth.tenantId, primaryCollectionId, question.id);
|
||||
|
||||
const versionResult = await client.query(
|
||||
`
|
||||
@@ -93,6 +169,22 @@ export async function updateQuestionRoute(ctx: RequestContext) {
|
||||
const createVersion = body.createVersion === true;
|
||||
|
||||
const item = await transaction(async client => {
|
||||
const questionBankId = nullableString(body.questionBankId);
|
||||
const subjectId = nullableString(body.subjectId);
|
||||
const categoryId = nullableString(body.categoryId);
|
||||
const nodeId = nullableString(body.nodeId);
|
||||
const entryId = nullableString(body.entryId);
|
||||
const contentNodeId = nullableString(body.contentNodeId);
|
||||
const primaryCollectionId = nullableString(body.primaryCollectionId) || nullableString(body.collectionId);
|
||||
|
||||
await assertOptionalReference(client, auth.tenantId, 'question_banks', questionBankId, 'QUESTION_BANK_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'subjects', subjectId, 'SUBJECT_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'categories', categoryId, 'CATEGORY_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'module_nodes', nodeId, 'NODE_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'content_entries', entryId, 'ENTRY_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'content_nodes', contentNodeId, 'CONTENT_NODE_NOT_FOUND');
|
||||
await assertOptionalReference(client, auth.tenantId, 'question_collections', primaryCollectionId, 'QUESTION_COLLECTION_NOT_FOUND');
|
||||
|
||||
const questionResult = await client.query(
|
||||
`
|
||||
update public.questions
|
||||
@@ -100,26 +192,36 @@ export async function updateQuestionRoute(ctx: RequestContext) {
|
||||
subject_id = coalesce($4::uuid, subject_id),
|
||||
category_id = coalesce($5::uuid, category_id),
|
||||
node_id = coalesce($6::uuid, node_id),
|
||||
type = coalesce(nullif($7, ''), type),
|
||||
type_label = coalesce(nullif($8, ''), type_label),
|
||||
difficulty = coalesce($9, difficulty),
|
||||
tags = case when $10::boolean then $11::jsonb else tags end,
|
||||
media_url = coalesce(nullif($12, ''), media_url),
|
||||
status = coalesce(nullif($13, ''), status),
|
||||
entry_id = coalesce($7::uuid, entry_id),
|
||||
content_node_id = coalesce($8::uuid, content_node_id),
|
||||
primary_collection_id = coalesce($9::uuid, primary_collection_id),
|
||||
type = coalesce(nullif($10, ''), type),
|
||||
type_label = coalesce(nullif($11, ''), type_label),
|
||||
difficulty = coalesce($12, difficulty),
|
||||
tags = case when $13::boolean then $14::jsonb else tags end,
|
||||
media_url = coalesce(nullif($15, ''), media_url),
|
||||
status = coalesce(nullif($16, ''), status),
|
||||
exam_markers = case when $17::boolean then $18::jsonb else exam_markers end,
|
||||
updated_at = now()
|
||||
where tenant_id = $1 and id = $2
|
||||
returning id, tenant_id as "tenantId", question_bank_id as "questionBankId",
|
||||
subject_id as "subjectId", category_id as "categoryId", node_id as "nodeId",
|
||||
type, type_label as "typeLabel", difficulty, tags, media_url as "mediaUrl",
|
||||
status, current_version_id as "currentVersionId", updated_at as "updatedAt"
|
||||
entry_id as "entryId", content_node_id as "contentNodeId",
|
||||
primary_collection_id as "primaryCollectionId", type,
|
||||
type_label as "typeLabel", difficulty, tags, media_url as "mediaUrl",
|
||||
status, exam_markers as "examMarkers",
|
||||
current_version_id as "currentVersionId", updated_at as "updatedAt"
|
||||
`,
|
||||
[
|
||||
auth.tenantId,
|
||||
questionId,
|
||||
nullableString(body.questionBankId),
|
||||
nullableString(body.subjectId),
|
||||
nullableString(body.categoryId),
|
||||
nullableString(body.nodeId),
|
||||
questionBankId,
|
||||
subjectId,
|
||||
categoryId,
|
||||
nodeId,
|
||||
entryId,
|
||||
contentNodeId,
|
||||
primaryCollectionId,
|
||||
optionalString(body, 'type'),
|
||||
optionalString(body, 'typeLabel'),
|
||||
body.difficulty === undefined ? null : intValue(body.difficulty, 1),
|
||||
@@ -127,11 +229,14 @@ export async function updateQuestionRoute(ctx: RequestContext) {
|
||||
jsonArrayValue(body.tags),
|
||||
nullableString(body.mediaUrl),
|
||||
body.status ? optionalStatus(body.status, QUESTION_STATUSES, 'published') : '',
|
||||
Object.hasOwn(body, 'examMarkers'),
|
||||
jsonObjectValue(body.examMarkers),
|
||||
],
|
||||
);
|
||||
|
||||
const question = questionResult.rows[0];
|
||||
if (!question) throw new HttpError(404, 'Question not found', 'QUESTION_NOT_FOUND');
|
||||
await syncPrimaryCollectionItem(client, auth.tenantId, question.primaryCollectionId || null, question.id);
|
||||
|
||||
if (!createVersion) return question;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user