Files
classroomio/packages/db/src/queries/lesson/video-progress.ts
T
Rotimi Best 52d9bf18c3 feat: add sequential progression and video watch lesson completion
Gate student content by completion order, track per-video watch progress
with seek enforcement, and auto-complete lessons at the configured
threshold. Show ring progress in the lesson header, keep watch progress
on rewatch, and sync progression locks client-side so the next item
unlocks after completion.
2026-06-11 14:48:34 +01:00

104 lines
3.4 KiB
TypeScript

import * as schema from '@db/schema';
import type { TLessonVideoProgress, TNewLessonVideoProgress } from '@db/types';
import { and, db, eq } from '@db/drizzle';
export async function getLessonVideoProgress(
lessonId: string,
profileId: string,
assetId: string
): Promise<TLessonVideoProgress | null> {
try {
const [row] = await db
.select()
.from(schema.lessonVideoProgress)
.where(
and(
eq(schema.lessonVideoProgress.lessonId, lessonId),
eq(schema.lessonVideoProgress.profileId, profileId),
eq(schema.lessonVideoProgress.assetId, assetId)
)
)
.limit(1);
return row ?? null;
} catch (error) {
console.error('getLessonVideoProgress error:', error);
throw new Error(
`Failed to get lesson video progress for lesson "${lessonId}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function getLessonVideoProgressForLesson(
lessonId: string,
profileId: string
): Promise<TLessonVideoProgress[]> {
try {
return db
.select()
.from(schema.lessonVideoProgress)
.where(
and(eq(schema.lessonVideoProgress.lessonId, lessonId), eq(schema.lessonVideoProgress.profileId, profileId))
);
} catch (error) {
console.error('getLessonVideoProgressForLesson error:', error);
throw new Error(
`Failed to get lesson video progress rows for lesson "${lessonId}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
function normalizeLessonVideoProgressValues(
data: TNewLessonVideoProgress & { lessonId: string; profileId: string; assetId: string }
): TNewLessonVideoProgress & { lessonId: string; profileId: string; assetId: string } {
return {
...data,
durationSeconds: data.durationSeconds != null ? Math.round(data.durationSeconds) : data.durationSeconds,
watchedSeconds: data.watchedSeconds != null ? Math.round(data.watchedSeconds) : data.watchedSeconds,
furthestSeconds: data.furthestSeconds != null ? Math.floor(data.furthestSeconds) : data.furthestSeconds,
lastPositionSeconds:
data.lastPositionSeconds != null ? Math.floor(data.lastPositionSeconds) : data.lastPositionSeconds
};
}
export async function upsertLessonVideoProgress(
data: TNewLessonVideoProgress & { lessonId: string; profileId: string; assetId: string }
): Promise<TLessonVideoProgress> {
try {
const normalized = normalizeLessonVideoProgressValues(data);
const existing = await getLessonVideoProgress(normalized.lessonId, normalized.profileId, normalized.assetId);
const now = new Date().toISOString();
if (existing) {
const [updated] = await db
.update(schema.lessonVideoProgress)
.set({ ...normalized, updatedAt: now })
.where(eq(schema.lessonVideoProgress.id, existing.id))
.returning();
if (!updated) {
throw new Error('Failed to update lesson video progress');
}
return updated;
}
const [inserted] = await db
.insert(schema.lessonVideoProgress)
.values({ ...normalized, createdAt: now, updatedAt: now })
.returning();
if (!inserted) {
throw new Error('Failed to insert lesson video progress');
}
return inserted;
} catch (error) {
console.error('upsertLessonVideoProgress error:', error);
throw new Error(
`Failed to upsert lesson video progress for lesson "${data.lessonId}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}