feat(routes): thread userId through routes + visibility/fork/curated endpoints
This commit is contained in:
17
packages/backend/src/routes/admin-lessons.ts
Normal file
17
packages/backend/src/routes/admin-lessons.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { adminLessonCuratedSchema } from '@flashcard/shared';
|
||||||
|
import type { Db } from '../db/client.js';
|
||||||
|
import { setLessonCurated } from '../services/lessons.js';
|
||||||
|
|
||||||
|
export function adminLessonsRouter(db: Db): Router {
|
||||||
|
const r = Router();
|
||||||
|
|
||||||
|
r.patch('/:id/curated', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const input = adminLessonCuratedSchema.parse(req.body);
|
||||||
|
res.json(await setLessonCurated(db, Number(req.params.id), input.isCurated));
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
@@ -2,38 +2,39 @@ import { Router } from 'express';
|
|||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import { cardCreateSchema, cardUpdateSchema } from '@flashcard/shared';
|
import { cardCreateSchema, cardUpdateSchema } from '@flashcard/shared';
|
||||||
import type { Db } from '../db/client.js';
|
import type { Db } from '../db/client.js';
|
||||||
import { ApiError } from '../lib/errors.js';
|
|
||||||
import { createCard, deleteCard, getCard, listCardsByLesson, updateCard } from '../services/cards.js';
|
import { createCard, deleteCard, getCard, listCardsByLesson, updateCard } from '../services/cards.js';
|
||||||
|
import { ApiError } from '../lib/errors.js';
|
||||||
import { exportCardsToBuffer, importCardsFromBuffer } from '../services/import.js';
|
import { exportCardsToBuffer, importCardsFromBuffer } from '../services/import.js';
|
||||||
|
|
||||||
|
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
|
||||||
|
|
||||||
export function cardsRouter(db: Db): Router {
|
export function cardsRouter(db: Db): Router {
|
||||||
const r = Router({ mergeParams: true });
|
const r = Router({ mergeParams: true });
|
||||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
|
|
||||||
|
|
||||||
r.get('/lessons/:lessonId/cards', async (req, res, next) => {
|
r.get('/lessons/:lessonId/cards', async (req, res, next) => {
|
||||||
try { res.json(await listCardsByLesson(db, Number(req.params.lessonId))); } catch (e) { next(e); }
|
try { res.json(await listCardsByLesson(db, req.user!.id, Number(req.params.lessonId))); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.post('/lessons/:lessonId/cards', async (req, res, next) => {
|
r.post('/lessons/:lessonId/cards', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const input = cardCreateSchema.parse(req.body);
|
const input = cardCreateSchema.parse(req.body);
|
||||||
res.status(201).json(await createCard(db, Number(req.params.lessonId), input));
|
res.status(201).json(await createCard(db, req.user!.id, Number(req.params.lessonId), input));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.get('/cards/:id', async (req, res, next) => {
|
r.get('/cards/:id', async (req, res, next) => {
|
||||||
try { res.json(await getCard(db, Number(req.params.id))); } catch (e) { next(e); }
|
try { res.json(await getCard(db, req.user!.id, Number(req.params.id))); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.patch('/cards/:id', async (req, res, next) => {
|
r.patch('/cards/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const input = cardUpdateSchema.parse(req.body);
|
const input = cardUpdateSchema.parse(req.body);
|
||||||
res.json(await updateCard(db, Number(req.params.id), input));
|
res.json(await updateCard(db, req.user!.id, Number(req.params.id), input));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.delete('/cards/:id', async (req, res, next) => {
|
r.delete('/cards/:id', async (req, res, next) => {
|
||||||
try { await deleteCard(db, Number(req.params.id)); res.status(204).end(); } catch (e) { next(e); }
|
try { await deleteCard(db, req.user!.id, Number(req.params.id)); res.status(204).end(); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.post('/lessons/:lessonId/cards/import', upload.single('file'), async (req, res, next) => {
|
r.post('/lessons/:lessonId/cards/import', upload.single('file'), async (req, res, next) => {
|
||||||
@@ -42,7 +43,8 @@ export function cardsRouter(db: Db): Router {
|
|||||||
const updateExisting = req.body.updateExisting !== 'false';
|
const updateExisting = req.body.updateExisting !== 'false';
|
||||||
const createMissingLessons = req.body.createMissingLessons === 'true';
|
const createMissingLessons = req.body.createMissingLessons === 'true';
|
||||||
const result = await importCardsFromBuffer(
|
const result = await importCardsFromBuffer(
|
||||||
db, Number(req.params.lessonId), req.file.buffer, { updateExisting, createMissingLessons }
|
db, req.user!.id, Number(req.params.lessonId), req.file.buffer,
|
||||||
|
{ updateExisting, createMissingLessons }
|
||||||
);
|
);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
@@ -51,7 +53,7 @@ export function cardsRouter(db: Db): Router {
|
|||||||
r.get('/lessons/:lessonId/cards/export', async (req, res, next) => {
|
r.get('/lessons/:lessonId/cards/export', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const includeDescendants = req.query.include_descendants === 'true';
|
const includeDescendants = req.query.include_descendants === 'true';
|
||||||
const buf = await exportCardsToBuffer(db, Number(req.params.lessonId), includeDescendants);
|
const buf = await exportCardsToBuffer(db, req.user!.id, Number(req.params.lessonId), includeDescendants);
|
||||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="cards-lesson-${req.params.lessonId}.xlsx"`);
|
res.setHeader('Content-Disposition', `attachment; filename="cards-lesson-${req.params.lessonId}.xlsx"`);
|
||||||
res.send(buf);
|
res.send(buf);
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { lessonCreateSchema, lessonMoveSchema, lessonUpdateSchema } from '@flashcard/shared';
|
import {
|
||||||
|
lessonCreateSchema, lessonMoveSchema, lessonUpdateSchema, lessonVisibilityUpdateSchema,
|
||||||
|
} from '@flashcard/shared';
|
||||||
import type { Db } from '../db/client.js';
|
import type { Db } from '../db/client.js';
|
||||||
import {
|
import {
|
||||||
createLesson, deleteLesson, getLessonTree, moveLesson, updateLesson,
|
createLesson, deleteLesson, getLessonTree, moveLesson, setLessonVisibility, updateLesson,
|
||||||
} from '../services/lessons.js';
|
} from '../services/lessons.js';
|
||||||
|
import { forkLesson } from '../services/fork.js';
|
||||||
|
|
||||||
export function lessonsRouter(db: Db): Router {
|
export function lessonsRouter(db: Db): Router {
|
||||||
const r = Router();
|
const r = Router();
|
||||||
|
|
||||||
r.get('/tree', async (_req, res, next) => {
|
r.get('/tree', async (req, res, next) => {
|
||||||
try { res.json(await getLessonTree(db)); } catch (e) { next(e); }
|
try { res.json(await getLessonTree(db, req.user!.id)); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.post('/', async (req, res, next) => {
|
r.post('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const input = lessonCreateSchema.parse(req.body);
|
const input = lessonCreateSchema.parse(req.body);
|
||||||
res.status(201).json(await createLesson(db, input));
|
res.status(201).json(await createLesson(db, req.user!.id, input));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.patch('/:id', async (req, res, next) => {
|
r.patch('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const id = Number(req.params.id);
|
|
||||||
const input = lessonUpdateSchema.parse(req.body);
|
const input = lessonUpdateSchema.parse(req.body);
|
||||||
res.json(await updateLesson(db, id, input));
|
res.json(await updateLesson(db, req.user!.id, Number(req.params.id), input));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.delete('/:id', async (req, res, next) => {
|
r.delete('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await deleteLesson(db, Number(req.params.id));
|
await deleteLesson(db, req.user!.id, Number(req.params.id));
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
@@ -37,7 +39,20 @@ export function lessonsRouter(db: Db): Router {
|
|||||||
r.post('/:id/move', async (req, res, next) => {
|
r.post('/:id/move', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const input = lessonMoveSchema.parse(req.body);
|
const input = lessonMoveSchema.parse(req.body);
|
||||||
res.json(await moveLesson(db, Number(req.params.id), input));
|
res.json(await moveLesson(db, req.user!.id, Number(req.params.id), input));
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
r.patch('/:id/visibility', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const input = lessonVisibilityUpdateSchema.parse(req.body);
|
||||||
|
res.json(await setLessonVisibility(db, req.user!.id, Number(req.params.id), input.visibility));
|
||||||
|
} catch (e) { next(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
r.post('/:id/fork', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
res.status(201).json(await forkLesson(db, req.user!.id, Number(req.params.id)));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ export function sessionsRouter(db: Db): Router {
|
|||||||
r.post('/', async (req, res, next) => {
|
r.post('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const input = sessionStartSchema.parse(req.body);
|
const input = sessionStartSchema.parse(req.body);
|
||||||
res.status(201).json(await startSession(db, input));
|
res.status(201).json(await startSession(db, req.user!.id, input));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.get('/active', async (_req, res, next) => {
|
r.get('/active', async (req, res, next) => {
|
||||||
try { res.json(await getActiveSession(db)); } catch (e) { next(e); }
|
try { res.json(await getActiveSession(db, req.user!.id)); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.get('/:id', async (req, res, next) => {
|
r.get('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const state = await getSessionState(db, Number(req.params.id));
|
const state = await getSessionState(db, req.user!.id, Number(req.params.id));
|
||||||
if (!state) throw ApiError.notFound('Session');
|
if (!state) throw ApiError.notFound('Session');
|
||||||
res.json(state);
|
res.json(state);
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
@@ -31,7 +31,7 @@ export function sessionsRouter(db: Db): Router {
|
|||||||
|
|
||||||
r.get('/:id/next', async (req, res, next) => {
|
r.get('/:id/next', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const item = await getNextItem(db, Number(req.params.id));
|
const item = await getNextItem(db, req.user!.id, Number(req.params.id));
|
||||||
if (!item) { res.json({ done: true }); return; }
|
if (!item) { res.json({ done: true }); return; }
|
||||||
res.json({ done: false, item });
|
res.json({ done: false, item });
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
@@ -40,17 +40,17 @@ export function sessionsRouter(db: Db): Router {
|
|||||||
r.post('/:id/attempts', async (req, res, next) => {
|
r.post('/:id/attempts', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const input = attemptCreateSchema.parse(req.body);
|
const input = attemptCreateSchema.parse(req.body);
|
||||||
await recordAttempt(db, Number(req.params.id), input);
|
await recordAttempt(db, req.user!.id, Number(req.params.id), input);
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.post('/:id/end', async (req, res, next) => {
|
r.post('/:id/end', async (req, res, next) => {
|
||||||
try { res.json(await endSession(db, Number(req.params.id))); } catch (e) { next(e); }
|
try { res.json(await endSession(db, req.user!.id, Number(req.params.id))); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
r.post('/:id/abandon', async (req, res, next) => {
|
r.post('/:id/abandon', async (req, res, next) => {
|
||||||
try { res.json(await abandonSession(db, Number(req.params.id))); } catch (e) { next(e); }
|
try { res.json(await abandonSession(db, req.user!.id, Number(req.params.id))); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
|
|||||||
@@ -4,20 +4,22 @@ import { getCardStats, getHeatmap, getLessonStats, getOverview } from '../servic
|
|||||||
|
|
||||||
export function statsRouter(db: Db): Router {
|
export function statsRouter(db: Db): Router {
|
||||||
const r = Router();
|
const r = Router();
|
||||||
r.get('/overview', async (_req, res, next) => {
|
|
||||||
try { res.json(await getOverview(db)); } catch (e) { next(e); }
|
r.get('/overview', async (req, res, next) => {
|
||||||
|
try { res.json(await getOverview(db, req.user!.id)); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
r.get('/lessons/:id', async (req, res, next) => {
|
r.get('/lessons/:id', async (req, res, next) => {
|
||||||
try { res.json(await getLessonStats(db, Number(req.params.id))); } catch (e) { next(e); }
|
try { res.json(await getLessonStats(db, req.user!.id, Number(req.params.id))); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
r.get('/cards/:id', async (req, res, next) => {
|
r.get('/cards/:id', async (req, res, next) => {
|
||||||
try { res.json(await getCardStats(db, Number(req.params.id))); } catch (e) { next(e); }
|
try { res.json(await getCardStats(db, req.user!.id, Number(req.params.id))); } catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
r.get('/heatmap', async (req, res, next) => {
|
r.get('/heatmap', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const weeks = Math.min(52, Math.max(1, Number(req.query.weeks ?? 12)));
|
const weeks = Math.min(52, Math.max(1, Number(req.query.weeks ?? 12)));
|
||||||
res.json(await getHeatmap(db, weeks));
|
res.json(await getHeatmap(db, req.user!.id, weeks));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { makeTestDb } from '../tests/dbHelper.js';
|
import { makeTestDb, createUserDirect, createLessonOwnedBy } from '../tests/dbHelper.js';
|
||||||
import { createLesson } from './lessons.js';
|
|
||||||
import { importCardsFromBuffer, exportCardsToBuffer } from './import.js';
|
import { importCardsFromBuffer, exportCardsToBuffer } from './import.js';
|
||||||
import { listCardsByLesson } from './cards.js';
|
import { listCardsByLesson } from './cards.js';
|
||||||
|
|
||||||
let env: ReturnType<typeof makeTestDb>;
|
let env: ReturnType<typeof makeTestDb>;
|
||||||
beforeEach(() => { env = makeTestDb(); });
|
let owner: Awaited<ReturnType<typeof createUserDirect>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env = makeTestDb();
|
||||||
|
owner = await createUserDirect(env.db, { email: 'owner@test.local' });
|
||||||
|
});
|
||||||
|
|
||||||
function buildXlsx(rows: Record<string, string>[]): Buffer {
|
function buildXlsx(rows: Record<string, string>[]): Buffer {
|
||||||
const ws = XLSX.utils.json_to_sheet(rows);
|
const ws = XLSX.utils.json_to_sheet(rows);
|
||||||
@@ -17,39 +21,39 @@ function buildXlsx(rows: Record<string, string>[]): Buffer {
|
|||||||
|
|
||||||
describe('excel import', () => {
|
describe('excel import', () => {
|
||||||
it('imports cards into the target lesson when no lesson_path column present', async () => {
|
it('imports cards into the target lesson when no lesson_path column present', async () => {
|
||||||
const l = await createLesson(env.db, { name: 'L' });
|
const l = await createLessonOwnedBy(env.db, owner.id, { name: 'L' });
|
||||||
const buf = buildXlsx([
|
const buf = buildXlsx([
|
||||||
{ question: 'q1', answer: 'a1' },
|
{ question: 'q1', answer: 'a1' },
|
||||||
{ question: 'q2', answer: 'a2', hint: 'tip' },
|
{ question: 'q2', answer: 'a2', hint: 'tip' },
|
||||||
]);
|
]);
|
||||||
const result = await importCardsFromBuffer(env.db, l.id, buf, { updateExisting: true, createMissingLessons: false });
|
const result = await importCardsFromBuffer(env.db, owner.id, l.id, buf, { updateExisting: true, createMissingLessons: false });
|
||||||
expect(result.inserted).toBe(2);
|
expect(result.inserted).toBe(2);
|
||||||
expect(result.errors).toHaveLength(0);
|
expect(result.errors).toHaveLength(0);
|
||||||
expect(await listCardsByLesson(env.db, l.id)).toHaveLength(2);
|
expect(await listCardsByLesson(env.db, owner.id, l.id)).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates intermediate lessons from lesson_path when allowed', async () => {
|
it('creates intermediate lessons from lesson_path when allowed', async () => {
|
||||||
const root = await createLesson(env.db, { name: 'Spaans' });
|
const root = await createLessonOwnedBy(env.db, owner.id, { name: 'Spaans' });
|
||||||
const buf = buildXlsx([
|
const buf = buildXlsx([
|
||||||
{ question: 'hola', answer: 'hello', lesson_path: 'Spaans/Begroetingen' },
|
{ question: 'hola', answer: 'hello', lesson_path: 'Spaans/Begroetingen' },
|
||||||
]);
|
]);
|
||||||
const result = await importCardsFromBuffer(env.db, root.id, buf, { updateExisting: true, createMissingLessons: true });
|
const result = await importCardsFromBuffer(env.db, owner.id, root.id, buf, { updateExisting: true, createMissingLessons: true });
|
||||||
expect(result.inserted).toBe(1);
|
expect(result.inserted).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates an existing card on duplicate question in same lesson', async () => {
|
it('updates an existing card on duplicate question in same lesson', async () => {
|
||||||
const l = await createLesson(env.db, { name: 'L' });
|
const l = await createLessonOwnedBy(env.db, owner.id, { name: 'L' });
|
||||||
await importCardsFromBuffer(env.db, l.id, buildXlsx([{ question: 'q', answer: 'a' }]), { updateExisting: true, createMissingLessons: false });
|
await importCardsFromBuffer(env.db, owner.id, l.id, buildXlsx([{ question: 'q', answer: 'a' }]), { updateExisting: true, createMissingLessons: false });
|
||||||
const res = await importCardsFromBuffer(env.db, l.id, buildXlsx([{ question: 'q', answer: 'b' }]), { updateExisting: true, createMissingLessons: false });
|
const res = await importCardsFromBuffer(env.db, owner.id, l.id, buildXlsx([{ question: 'q', answer: 'b' }]), { updateExisting: true, createMissingLessons: false });
|
||||||
expect(res.updated).toBe(1);
|
expect(res.updated).toBe(1);
|
||||||
const cards = await listCardsByLesson(env.db, l.id);
|
const cards = await listCardsByLesson(env.db, owner.id, l.id);
|
||||||
expect(cards[0]!.answer).toBe('b');
|
expect(cards[0]!.answer).toBe('b');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exports a buffer that round-trips back via import', async () => {
|
it('exports a buffer that round-trips back via import', async () => {
|
||||||
const l = await createLesson(env.db, { name: 'L' });
|
const l = await createLessonOwnedBy(env.db, owner.id, { name: 'L' });
|
||||||
await importCardsFromBuffer(env.db, l.id, buildXlsx([{ question: 'q', answer: 'a' }]), { updateExisting: true, createMissingLessons: false });
|
await importCardsFromBuffer(env.db, owner.id, l.id, buildXlsx([{ question: 'q', answer: 'a' }]), { updateExisting: true, createMissingLessons: false });
|
||||||
const buf = await exportCardsToBuffer(env.db, l.id, false);
|
const buf = await exportCardsToBuffer(env.db, owner.id, l.id, false);
|
||||||
const wb = XLSX.read(buf, { type: 'buffer' });
|
const wb = XLSX.read(buf, { type: 'buffer' });
|
||||||
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]!]!);
|
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]!]!);
|
||||||
expect(rows).toHaveLength(1);
|
expect(rows).toHaveLength(1);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import * as XLSX from 'xlsx';
|
|||||||
import type { Db } from '../db/client.js';
|
import type { Db } from '../db/client.js';
|
||||||
import { cardProgress, cards, lessons } from '../db/schema.js';
|
import { cardProgress, cards, lessons } from '../db/schema.js';
|
||||||
import { getDescendantLessonIds } from './lessons.js';
|
import { getDescendantLessonIds } from './lessons.js';
|
||||||
|
import { canEditLesson, canReadLesson } from './permissions.js';
|
||||||
|
import { ApiError } from '../lib/errors.js';
|
||||||
|
|
||||||
export interface ImportRow {
|
export interface ImportRow {
|
||||||
question: string;
|
question: string;
|
||||||
@@ -55,8 +57,11 @@ async function resolveLesson(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function importCardsFromBuffer(
|
export async function importCardsFromBuffer(
|
||||||
db: Db, defaultLessonId: number, buffer: Buffer, opts: ImportOptions
|
db: Db, userId: number, defaultLessonId: number, buffer: Buffer, opts: ImportOptions
|
||||||
): Promise<ImportResult> {
|
): Promise<ImportResult> {
|
||||||
|
if (!(await canEditLesson(db, userId, defaultLessonId))) {
|
||||||
|
throw new ApiError(403, 'FORBIDDEN_LESSON', 'Not your lesson');
|
||||||
|
}
|
||||||
const rows = parseSheet(buffer);
|
const rows = parseSheet(buffer);
|
||||||
const result: ImportResult = { inserted: 0, updated: 0, skipped: 0, errors: [] };
|
const result: ImportResult = { inserted: 0, updated: 0, skipped: 0, errors: [] };
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
@@ -94,7 +99,12 @@ export async function importCardsFromBuffer(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportCardsToBuffer(db: Db, lessonId: number, includeDescendants: boolean): Promise<Buffer> {
|
export async function exportCardsToBuffer(
|
||||||
|
db: Db, userId: number, lessonId: number, includeDescendants: boolean
|
||||||
|
): Promise<Buffer> {
|
||||||
|
if (!(await canReadLesson(db, userId, lessonId))) {
|
||||||
|
throw new ApiError(403, 'FORBIDDEN_LESSON', 'Cannot read this lesson');
|
||||||
|
}
|
||||||
const ids = includeDescendants ? await getDescendantLessonIds(db, lessonId) : [lessonId];
|
const ids = includeDescendants ? await getDescendantLessonIds(db, lessonId) : [lessonId];
|
||||||
const lessonRows = db.select().from(lessons).where(inArray(lessons.id, ids)).all();
|
const lessonRows = db.select().from(lessons).where(inArray(lessons.id, ids)).all();
|
||||||
const pathById = buildPathMap(lessonRows);
|
const pathById = buildPathMap(lessonRows);
|
||||||
|
|||||||
Reference in New Issue
Block a user