@goscribe/server 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context.d.ts +5 -1
- package/dist/lib/activity_human_description.d.ts +13 -0
- package/dist/lib/activity_human_description.js +221 -0
- package/dist/lib/activity_human_description.test.d.ts +1 -0
- package/dist/lib/activity_human_description.test.js +16 -0
- package/dist/lib/activity_log_service.d.ts +87 -0
- package/dist/lib/activity_log_service.js +276 -0
- package/dist/lib/activity_log_service.test.d.ts +1 -0
- package/dist/lib/activity_log_service.test.js +27 -0
- package/dist/lib/ai-session.d.ts +15 -2
- package/dist/lib/ai-session.js +147 -85
- package/dist/lib/constants.d.ts +13 -0
- package/dist/lib/constants.js +12 -0
- package/dist/lib/email.d.ts +11 -0
- package/dist/lib/email.js +193 -0
- package/dist/lib/env.d.ts +13 -0
- package/dist/lib/env.js +16 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +3 -3
- package/dist/lib/logger.d.ts +4 -4
- package/dist/lib/logger.js +30 -8
- package/dist/lib/notification-service.d.ts +152 -0
- package/dist/lib/notification-service.js +473 -0
- package/dist/lib/notification-service.test.d.ts +1 -0
- package/dist/lib/notification-service.test.js +87 -0
- package/dist/lib/prisma.d.ts +2 -1
- package/dist/lib/prisma.js +5 -1
- package/dist/lib/pusher.d.ts +23 -0
- package/dist/lib/pusher.js +69 -5
- package/dist/lib/retry.d.ts +15 -0
- package/dist/lib/retry.js +37 -0
- package/dist/lib/storage.js +2 -2
- package/dist/lib/stripe.d.ts +9 -0
- package/dist/lib/stripe.js +36 -0
- package/dist/lib/subscription_service.d.ts +37 -0
- package/dist/lib/subscription_service.js +654 -0
- package/dist/lib/usage_service.d.ts +26 -0
- package/dist/lib/usage_service.js +59 -0
- package/dist/lib/worksheet-generation.d.ts +91 -0
- package/dist/lib/worksheet-generation.js +95 -0
- package/dist/lib/worksheet-generation.test.d.ts +1 -0
- package/dist/lib/worksheet-generation.test.js +20 -0
- package/dist/lib/workspace-access.d.ts +18 -0
- package/dist/lib/workspace-access.js +13 -0
- package/dist/routers/_app.d.ts +1349 -253
- package/dist/routers/_app.js +10 -0
- package/dist/routers/admin.d.ts +361 -0
- package/dist/routers/admin.js +633 -0
- package/dist/routers/annotations.d.ts +219 -0
- package/dist/routers/annotations.js +187 -0
- package/dist/routers/auth.d.ts +88 -7
- package/dist/routers/auth.js +339 -19
- package/dist/routers/chat.d.ts +6 -12
- package/dist/routers/copilot.d.ts +199 -0
- package/dist/routers/copilot.js +571 -0
- package/dist/routers/flashcards.d.ts +47 -81
- package/dist/routers/flashcards.js +143 -27
- package/dist/routers/members.d.ts +36 -7
- package/dist/routers/members.js +200 -19
- package/dist/routers/notifications.d.ts +99 -0
- package/dist/routers/notifications.js +127 -0
- package/dist/routers/payment.d.ts +89 -0
- package/dist/routers/payment.js +403 -0
- package/dist/routers/podcast.d.ts +8 -13
- package/dist/routers/podcast.js +54 -31
- package/dist/routers/studyguide.d.ts +1 -29
- package/dist/routers/studyguide.js +80 -71
- package/dist/routers/worksheets.d.ts +105 -38
- package/dist/routers/worksheets.js +258 -68
- package/dist/routers/workspace.d.ts +139 -60
- package/dist/routers/workspace.js +455 -315
- package/dist/scripts/purge-deleted-users.d.ts +1 -0
- package/dist/scripts/purge-deleted-users.js +149 -0
- package/dist/server.js +130 -10
- package/dist/services/flashcard-progress.service.d.ts +18 -66
- package/dist/services/flashcard-progress.service.js +51 -42
- package/dist/trpc.d.ts +20 -21
- package/dist/trpc.js +150 -1
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { prisma } from './prisma.js';
|
|
2
|
+
/**
|
|
3
|
+
* Counts all resources consumed by a user across the platform.
|
|
4
|
+
*/
|
|
5
|
+
export async function getUserUsage(userId) {
|
|
6
|
+
const [flashcards, worksheets, studyGuides, podcasts, storageResult] = await Promise.all([
|
|
7
|
+
prisma.artifact.count({ where: { createdById: userId, type: 'FLASHCARD_SET' } }),
|
|
8
|
+
prisma.artifact.count({ where: { createdById: userId, type: 'WORKSHEET' } }),
|
|
9
|
+
prisma.artifact.count({ where: { createdById: userId, type: 'STUDY_GUIDE' } }),
|
|
10
|
+
prisma.artifact.count({ where: { createdById: userId, type: 'PODCAST_EPISODE' } }),
|
|
11
|
+
prisma.fileAsset.aggregate({
|
|
12
|
+
_sum: { size: true },
|
|
13
|
+
where: { userId }
|
|
14
|
+
})
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
flashcards,
|
|
18
|
+
worksheets,
|
|
19
|
+
studyGuides,
|
|
20
|
+
podcasts,
|
|
21
|
+
storageBytes: Number(storageResult._sum.size || 0)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves the specific plan limits for a user based on their active subscription
|
|
26
|
+
* PLUS any extra credits purchased.
|
|
27
|
+
*/
|
|
28
|
+
export async function getUserPlanLimits(userId) {
|
|
29
|
+
const [activeSub, userCredits] = await Promise.all([
|
|
30
|
+
prisma.subscription.findFirst({
|
|
31
|
+
where: { userId, status: 'active' },
|
|
32
|
+
include: { plan: { include: { limit: true } } },
|
|
33
|
+
orderBy: { createdAt: 'desc' }
|
|
34
|
+
}),
|
|
35
|
+
prisma.userCredit.groupBy({
|
|
36
|
+
by: ['resourceType'],
|
|
37
|
+
where: { userId },
|
|
38
|
+
_sum: { amount: true }
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
const baseLimit = activeSub?.plan?.limit;
|
|
42
|
+
// If no active subscription and no credits, user has no active limits
|
|
43
|
+
if (!baseLimit && userCredits.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
// Fast lookup for credits
|
|
46
|
+
const getCreditSum = (type) => userCredits.find(c => c.resourceType === type)?._sum.amount || 0;
|
|
47
|
+
// Return the merged total limit
|
|
48
|
+
return {
|
|
49
|
+
id: baseLimit?.id || 'credits-only',
|
|
50
|
+
planId: baseLimit?.planId || 'credits-only',
|
|
51
|
+
maxStorageBytes: BigInt(Number(baseLimit?.maxStorageBytes || 0) + (getCreditSum('STORAGE') * 1024 * 1024)),
|
|
52
|
+
maxWorksheets: (baseLimit?.maxWorksheets || 0) + getCreditSum('WORKSHEET'),
|
|
53
|
+
maxFlashcards: (baseLimit?.maxFlashcards || 0) + getCreditSum('FLASHCARD_SET'),
|
|
54
|
+
maxPodcasts: (baseLimit?.maxPodcasts || 0) + getCreditSum('PODCAST_EPISODE'),
|
|
55
|
+
maxStudyGuides: (baseLimit?.maxStudyGuides || 0) + getCreditSum('STUDY_GUIDE'),
|
|
56
|
+
createdAt: baseLimit?.createdAt || new Date(),
|
|
57
|
+
updatedAt: baseLimit?.updatedAt || new Date(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Persisted on Artifact.worksheetConfig and inside generatingMetadata while generating */
|
|
3
|
+
export declare const worksheetModeSchema: z.ZodEnum<{
|
|
4
|
+
practice: "practice";
|
|
5
|
+
quiz: "quiz";
|
|
6
|
+
}>;
|
|
7
|
+
export type WorksheetMode = z.infer<typeof worksheetModeSchema>;
|
|
8
|
+
export declare const worksheetDifficultyInputSchema: z.ZodEnum<{
|
|
9
|
+
easy: "easy";
|
|
10
|
+
medium: "medium";
|
|
11
|
+
hard: "hard";
|
|
12
|
+
}>;
|
|
13
|
+
export type WorksheetDifficultyInput = z.infer<typeof worksheetDifficultyInputSchema>;
|
|
14
|
+
export declare const questionTypeSchema: z.ZodEnum<{
|
|
15
|
+
MULTIPLE_CHOICE: "MULTIPLE_CHOICE";
|
|
16
|
+
TEXT: "TEXT";
|
|
17
|
+
NUMERIC: "NUMERIC";
|
|
18
|
+
TRUE_FALSE: "TRUE_FALSE";
|
|
19
|
+
MATCHING: "MATCHING";
|
|
20
|
+
FILL_IN_THE_BLANK: "FILL_IN_THE_BLANK";
|
|
21
|
+
}>;
|
|
22
|
+
export type WorksheetQuestionTypeConfig = z.infer<typeof questionTypeSchema>;
|
|
23
|
+
/** Shape stored in WorksheetPreset.config and used for generation */
|
|
24
|
+
export declare const worksheetPresetConfigSchema: z.ZodObject<{
|
|
25
|
+
mode: z.ZodDefault<z.ZodEnum<{
|
|
26
|
+
practice: "practice";
|
|
27
|
+
quiz: "quiz";
|
|
28
|
+
}>>;
|
|
29
|
+
numQuestions: z.ZodDefault<z.ZodNumber>;
|
|
30
|
+
difficulty: z.ZodDefault<z.ZodEnum<{
|
|
31
|
+
easy: "easy";
|
|
32
|
+
medium: "medium";
|
|
33
|
+
hard: "hard";
|
|
34
|
+
}>>;
|
|
35
|
+
questionTypes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
36
|
+
MULTIPLE_CHOICE: "MULTIPLE_CHOICE";
|
|
37
|
+
TEXT: "TEXT";
|
|
38
|
+
NUMERIC: "NUMERIC";
|
|
39
|
+
TRUE_FALSE: "TRUE_FALSE";
|
|
40
|
+
MATCHING: "MATCHING";
|
|
41
|
+
FILL_IN_THE_BLANK: "FILL_IN_THE_BLANK";
|
|
42
|
+
}>>>;
|
|
43
|
+
mcqRatio: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
export type WorksheetPresetConfig = z.infer<typeof worksheetPresetConfigSchema>;
|
|
46
|
+
export declare const worksheetPresetConfigPartialSchema: z.ZodObject<{
|
|
47
|
+
mode: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
48
|
+
practice: "practice";
|
|
49
|
+
quiz: "quiz";
|
|
50
|
+
}>>>;
|
|
51
|
+
numQuestions: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
52
|
+
difficulty: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
|
|
53
|
+
easy: "easy";
|
|
54
|
+
medium: "medium";
|
|
55
|
+
hard: "hard";
|
|
56
|
+
}>>>;
|
|
57
|
+
questionTypes: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
58
|
+
MULTIPLE_CHOICE: "MULTIPLE_CHOICE";
|
|
59
|
+
TEXT: "TEXT";
|
|
60
|
+
NUMERIC: "NUMERIC";
|
|
61
|
+
TRUE_FALSE: "TRUE_FALSE";
|
|
62
|
+
MATCHING: "MATCHING";
|
|
63
|
+
FILL_IN_THE_BLANK: "FILL_IN_THE_BLANK";
|
|
64
|
+
}>>>>;
|
|
65
|
+
mcqRatio: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
/** Resolved config used by generateFromPrompt background job */
|
|
68
|
+
export type ResolvedWorksheetGeneration = WorksheetPresetConfig & {
|
|
69
|
+
prompt: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
estimatedTime?: string;
|
|
72
|
+
presetId?: string | null;
|
|
73
|
+
};
|
|
74
|
+
export declare function parsePresetConfig(raw: unknown): WorksheetPresetConfig;
|
|
75
|
+
export declare function mergeWorksheetGenerationConfig(presetConfig: Partial<WorksheetPresetConfig> | null | undefined, configOverride: Partial<WorksheetPresetConfig> | null | undefined, legacy: {
|
|
76
|
+
numQuestions?: number;
|
|
77
|
+
difficulty?: WorksheetDifficultyInput;
|
|
78
|
+
mode?: WorksheetMode;
|
|
79
|
+
}): WorksheetPresetConfig;
|
|
80
|
+
/** Normalize one LLM problem row before DB insert */
|
|
81
|
+
export declare function normalizeWorksheetProblemForDb(problem: Record<string, unknown>, index0: number, difficultyUpper: 'EASY' | 'MEDIUM' | 'HARD', forceMcq: boolean): {
|
|
82
|
+
prompt: string;
|
|
83
|
+
answer: string | null;
|
|
84
|
+
type: string;
|
|
85
|
+
difficulty: 'EASY' | 'MEDIUM' | 'HARD';
|
|
86
|
+
order: number;
|
|
87
|
+
meta: {
|
|
88
|
+
options?: string[];
|
|
89
|
+
markScheme?: unknown;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Persisted on Artifact.worksheetConfig and inside generatingMetadata while generating */
|
|
3
|
+
export const worksheetModeSchema = z.enum(['practice', 'quiz']);
|
|
4
|
+
export const worksheetDifficultyInputSchema = z.enum(['easy', 'medium', 'hard']);
|
|
5
|
+
export const questionTypeSchema = z.enum([
|
|
6
|
+
'MULTIPLE_CHOICE',
|
|
7
|
+
'TEXT',
|
|
8
|
+
'NUMERIC',
|
|
9
|
+
'TRUE_FALSE',
|
|
10
|
+
'MATCHING',
|
|
11
|
+
'FILL_IN_THE_BLANK',
|
|
12
|
+
]);
|
|
13
|
+
/** Shape stored in WorksheetPreset.config and used for generation */
|
|
14
|
+
export const worksheetPresetConfigSchema = z.object({
|
|
15
|
+
mode: worksheetModeSchema.default('practice'),
|
|
16
|
+
numQuestions: z.number().int().min(1).max(20).default(8),
|
|
17
|
+
difficulty: worksheetDifficultyInputSchema.default('medium'),
|
|
18
|
+
/** When omitted, quiz mode implies MCQ-only on the inference side */
|
|
19
|
+
questionTypes: z.array(questionTypeSchema).optional(),
|
|
20
|
+
/** 0–1 fraction of problems that should be MCQ (practice mode); ignored when quiz forces all MCQ */
|
|
21
|
+
mcqRatio: z.number().min(0).max(1).optional(),
|
|
22
|
+
});
|
|
23
|
+
export const worksheetPresetConfigPartialSchema = worksheetPresetConfigSchema.partial();
|
|
24
|
+
const defaultConfig = worksheetPresetConfigSchema.parse({});
|
|
25
|
+
export function parsePresetConfig(raw) {
|
|
26
|
+
return worksheetPresetConfigSchema.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
export function mergeWorksheetGenerationConfig(presetConfig, configOverride, legacy) {
|
|
29
|
+
// Precedence: legacy flat fields > configOverride > preset > defaults
|
|
30
|
+
// We place legacy AFTER configOverride because configOverride's Zod partial schema injects defaults (like medium)
|
|
31
|
+
// that shouldn't overwrite explicit explicit user choices passing via legacy parameters.
|
|
32
|
+
const merged = {
|
|
33
|
+
...defaultConfig,
|
|
34
|
+
...presetConfig,
|
|
35
|
+
...configOverride,
|
|
36
|
+
...(legacy.numQuestions !== undefined ? { numQuestions: legacy.numQuestions } : {}),
|
|
37
|
+
...(legacy.difficulty !== undefined ? { difficulty: legacy.difficulty } : {}),
|
|
38
|
+
...(legacy.mode !== undefined ? { mode: legacy.mode } : {}),
|
|
39
|
+
};
|
|
40
|
+
return worksheetPresetConfigSchema.parse(merged);
|
|
41
|
+
}
|
|
42
|
+
/** Normalize one LLM problem row before DB insert */
|
|
43
|
+
export function normalizeWorksheetProblemForDb(problem, index0, difficultyUpper, forceMcq) {
|
|
44
|
+
const prompt = (typeof problem.question === 'string' && problem.question) ||
|
|
45
|
+
(typeof problem.prompt === 'string' && problem.prompt) ||
|
|
46
|
+
`Question ${index0 + 1}`;
|
|
47
|
+
let type = String(problem.type || 'TEXT').toUpperCase().replace(/[\s-]/g, '_');
|
|
48
|
+
if (forceMcq)
|
|
49
|
+
type = 'MULTIPLE_CHOICE';
|
|
50
|
+
let options = [];
|
|
51
|
+
if (Array.isArray(problem.options)) {
|
|
52
|
+
options = problem.options.filter((o) => typeof o === 'string' && o.trim().length > 0);
|
|
53
|
+
}
|
|
54
|
+
let answer = (typeof problem.answer === 'string' && problem.answer) ||
|
|
55
|
+
(typeof problem.solution === 'string' && problem.solution) ||
|
|
56
|
+
'';
|
|
57
|
+
if (type === 'MULTIPLE_CHOICE') {
|
|
58
|
+
if (options.length < 2) {
|
|
59
|
+
options = ['Option A', 'Option B', 'Option C', 'Option D'];
|
|
60
|
+
}
|
|
61
|
+
const correctIndex = resolveMcqCorrectIndex(answer, options);
|
|
62
|
+
answer = String(correctIndex);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
prompt,
|
|
66
|
+
answer: answer || null,
|
|
67
|
+
type,
|
|
68
|
+
difficulty: difficultyUpper,
|
|
69
|
+
order: index0,
|
|
70
|
+
meta: {
|
|
71
|
+
...(options.length > 0 ? { options } : {}),
|
|
72
|
+
...(problem.mark_scheme !== undefined ? { markScheme: problem.mark_scheme } : {}),
|
|
73
|
+
// Non-MCQ questions are AI-marked via markScheme; provide a safe fallback
|
|
74
|
+
...(type === 'TEXT' && problem.mark_scheme === undefined
|
|
75
|
+
? {
|
|
76
|
+
markScheme: {
|
|
77
|
+
points: [{ point: 1, requirements: 'Typed answer matches expected content.' }],
|
|
78
|
+
totalPoints: 1,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function resolveMcqCorrectIndex(answer, options) {
|
|
86
|
+
const trimmed = answer.trim();
|
|
87
|
+
const asNum = parseInt(trimmed, 10);
|
|
88
|
+
if (!Number.isNaN(asNum) && asNum >= 0 && asNum < options.length) {
|
|
89
|
+
return asNum;
|
|
90
|
+
}
|
|
91
|
+
const byText = options.findIndex((o) => o.trim() === trimmed);
|
|
92
|
+
if (byText >= 0)
|
|
93
|
+
return byText;
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mergeWorksheetGenerationConfig, normalizeWorksheetProblemForDb } from './worksheet-generation.js';
|
|
4
|
+
test('mergeWorksheetGenerationConfig: override beats preset and legacy', () => {
|
|
5
|
+
const r = mergeWorksheetGenerationConfig({ mode: 'practice', numQuestions: 5, difficulty: 'easy' }, { mode: 'quiz', numQuestions: 10, difficulty: 'medium' }, { numQuestions: 3, difficulty: 'hard' });
|
|
6
|
+
assert.equal(r.mode, 'quiz');
|
|
7
|
+
assert.equal(r.numQuestions, 10);
|
|
8
|
+
assert.equal(r.difficulty, 'medium');
|
|
9
|
+
});
|
|
10
|
+
test('normalizeWorksheetProblemForDb coerces MCQ answer to option index', () => {
|
|
11
|
+
const row = normalizeWorksheetProblemForDb({
|
|
12
|
+
question: 'Capital of France?',
|
|
13
|
+
answer: 'Paris',
|
|
14
|
+
type: 'MULTIPLE_CHOICE',
|
|
15
|
+
options: ['London', 'Paris', 'Berlin'],
|
|
16
|
+
}, 0, 'MEDIUM', true);
|
|
17
|
+
assert.equal(row.type, 'MULTIPLE_CHOICE');
|
|
18
|
+
assert.equal(row.answer, '1');
|
|
19
|
+
assert.ok(row.meta.options && row.meta.options.length >= 2);
|
|
20
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma `where` filter for workspace access checks.
|
|
3
|
+
* Allows access if the user is the owner OR a member of the workspace.
|
|
4
|
+
* Use this instead of `{ ownerId: userId }` to support collaborative workspaces.
|
|
5
|
+
*/
|
|
6
|
+
export declare function workspaceAccessFilter(userId: string): {
|
|
7
|
+
OR: ({
|
|
8
|
+
ownerId: string;
|
|
9
|
+
members?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
members: {
|
|
12
|
+
some: {
|
|
13
|
+
userId: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
ownerId?: undefined;
|
|
17
|
+
})[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma `where` filter for workspace access checks.
|
|
3
|
+
* Allows access if the user is the owner OR a member of the workspace.
|
|
4
|
+
* Use this instead of `{ ownerId: userId }` to support collaborative workspaces.
|
|
5
|
+
*/
|
|
6
|
+
export function workspaceAccessFilter(userId) {
|
|
7
|
+
return {
|
|
8
|
+
OR: [
|
|
9
|
+
{ ownerId: userId },
|
|
10
|
+
{ members: { some: { userId } } },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
}
|