fix(import): set owner on auto-created lessons + nest lesson_path under started lesson

Two bugs surfaced by Excel import on a lesson with a lesson_path column:
1. resolveLesson created lessons without owner_id, so after the ownership
   model (sub-project B) they never appeared in getLessonTree — import
   reported success but nothing was visible.
2. lesson_path was resolved at the root; cards landed in new root lessons
   instead of under the lesson the import was started from.

Now: auto-created lessons get ownerId + visibility 'private'; lesson_path is
resolved relative to the started lesson (each segment a sublesson). Also drop
the stale eager card_progress insert (progress is per-user and lazy since B).
This commit is contained in:
2026-05-21 07:45:49 +02:00
parent 2890e19953
commit f5000d3c58
2 changed files with 45 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
import { and, eq, inArray, isNull } from 'drizzle-orm';
import { and, eq, inArray } from 'drizzle-orm';
import * as XLSX from 'xlsx';
import type { Db } from '../db/client.js';
import { cardProgress, cards, lessons } from '../db/schema.js';
import { cards, lessons } from '../db/schema.js';
import { getDescendantLessonIds } from './lessons.js';
import { canEditLesson, canReadLesson } from './permissions.js';
import { ApiError } from '../lib/errors.js';
@@ -36,6 +36,7 @@ function nowSec() { return Math.floor(Date.now() / 1000); }
async function resolveLesson(
db: Db,
userId: number,
defaultLessonId: number,
lessonPath: string | undefined,
createMissing: boolean
@@ -43,14 +44,19 @@ async function resolveLesson(
if (!lessonPath || lessonPath.trim() === '') return defaultLessonId;
const parts = lessonPath.split('/').map((s) => s.trim()).filter(Boolean);
if (parts.length === 0) return defaultLessonId;
let parentId: number | null = null;
// lesson_path is resolved RELATIVE to the lesson the import was started from:
// each segment becomes a sublesson under it (created on demand, owned by the user).
let parentId: number = defaultLessonId;
for (const name of parts) {
const found: typeof lessons.$inferSelect | undefined = parentId === null
? db.select().from(lessons).where(and(eq(lessons.name, name), isNull(lessons.parentId))).get()
: db.select().from(lessons).where(and(eq(lessons.name, name), eq(lessons.parentId, parentId))).get();
const found = db.select().from(lessons)
.where(and(eq(lessons.name, name), eq(lessons.parentId, parentId), eq(lessons.ownerId, userId)))
.get();
if (found) { parentId = found.id; continue; }
if (!createMissing) return null;
const [row] = db.insert(lessons).values({ name, parentId, position: 0 }).returning().all();
const [row] = db.insert(lessons).values({
name, parentId, position: 0,
ownerId: userId, visibility: 'private', isCurated: false,
}).returning().all();
parentId = row!.id;
}
return parentId;
@@ -73,7 +79,7 @@ export async function importCardsFromBuffer(
result.errors.push({ row: rowNum, message: 'question and answer are required' });
continue;
}
const targetLessonId = await resolveLesson(db, defaultLessonId, row.lesson_path, opts.createMissingLessons);
const targetLessonId = await resolveLesson(db, userId, defaultLessonId, row.lesson_path, opts.createMissingLessons);
if (targetLessonId === null) {
result.errors.push({ row: rowNum, message: `lesson_path not found: ${row.lesson_path}` });
continue;
@@ -87,12 +93,7 @@ export async function importCardsFromBuffer(
} else {
const positions = db.select({ pos: cards.position }).from(cards).where(eq(cards.lessonId, targetLessonId)).all();
const position = positions.length === 0 ? 0 : Math.max(...positions.map((p) => p.pos)) + 1;
const [inserted] = db.insert(cards).values({ lessonId: targetLessonId, question: q, answer: a, hint, position }).returning().all();
db.insert(cardProgress).values({ cardId: inserted!.id, direction: 'forward', box: 1, nextDueAt: 0 }).run();
const lesson = db.select().from(lessons).where(eq(lessons.id, targetLessonId)).get();
if (lesson?.bidirectional) {
db.insert(cardProgress).values({ cardId: inserted!.id, direction: 'backward', box: 1, nextDueAt: 0 }).run();
}
db.insert(cards).values({ lessonId: targetLessonId, question: q, answer: a, hint, position }).run();
result.inserted += 1;
}
}