@goscribe/server 1.5.0 → 1.6.0

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.
Files changed (41) hide show
  1. package/dist/context.d.ts +14 -1
  2. package/dist/context.js +23 -2
  3. package/dist/lib/ai/index.d.ts +3 -2
  4. package/dist/lib/ai/index.js +3 -2
  5. package/dist/lib/ai/llm-client.d.ts +1 -0
  6. package/dist/lib/ai/llm-client.js +17 -0
  7. package/dist/routers/_app.d.ts +40 -80
  8. package/dist/routers/auth.js +1 -1
  9. package/dist/routers/flashcards.d.ts +12 -1
  10. package/dist/routers/payment.d.ts +1 -12
  11. package/dist/routers/workspace.d.ts +27 -67
  12. package/dist/routers/workspace.js +1 -0
  13. package/dist/services/billing/payment.service.d.ts +1 -12
  14. package/dist/services/billing/payment.service.js +3 -6
  15. package/dist/services/billing/usage.service.d.ts +30 -10
  16. package/dist/services/billing/usage.service.js +87 -15
  17. package/dist/services/content/copilot.service.js +15 -29
  18. package/dist/services/content/flashcard-progress.service.js +9 -9
  19. package/dist/services/content/flashcard.service.d.ts +45 -1
  20. package/dist/services/content/flashcard.service.js +81 -68
  21. package/dist/services/content/media-analysis.service.js +27 -27
  22. package/dist/services/content/worksheet-generation.service.test.js +2 -2
  23. package/dist/services/workspace/workspace.service.d.ts +23 -67
  24. package/dist/services/workspace/workspace.service.js +69 -62
  25. package/dist/trpc.d.ts +12 -4
  26. package/dist/trpc.js +5 -11
  27. package/package.json +1 -1
  28. package/src/context.ts +33 -3
  29. package/src/lib/ai/index.ts +3 -0
  30. package/src/lib/ai/llm-client.ts +23 -0
  31. package/src/routers/auth.ts +1 -1
  32. package/src/routers/workspace.ts +4 -0
  33. package/src/services/billing/payment.service.ts +3 -6
  34. package/src/services/billing/usage.service.ts +190 -77
  35. package/src/services/content/copilot.service.ts +23 -32
  36. package/src/services/content/flashcard-progress.service.ts +12 -9
  37. package/src/services/content/flashcard.service.ts +89 -66
  38. package/src/services/content/media-analysis.service.ts +34 -29
  39. package/src/services/content/worksheet-generation.service.test.ts +2 -2
  40. package/src/services/workspace/workspace.service.ts +73 -66
  41. package/src/trpc.ts +5 -13
@@ -8,6 +8,7 @@ import { ai } from '../../lib/ai/index.js';
8
8
  import { workspaceKbService } from '../workspace/workspace-kb.service.js';
9
9
  import { getUserUsage, getUserPlanLimits } from '../billing/usage.service.js';
10
10
  import { notifyArtifactFailed, notifyArtifactReady } from '../notifications/notification.service.js';
11
+ import { FlashcardService } from './flashcard.service.js';
11
12
 
12
13
  type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'error';
13
14
  const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'] as const;
@@ -449,18 +450,34 @@ export class MediaAnalysisService extends BaseService {
449
450
  { ragContext: flashcardRag },
450
451
  );
451
452
 
452
- const artifact = await this.db.artifact.create({
453
- data: {
454
- workspaceId: input.workspaceId,
455
- type: ArtifactType.FLASHCARD_SET,
456
- title:
457
- files.length === 1
458
- ? `Flashcards - ${primaryFile.name}`
459
- : `Flashcards - ${files.length} files`,
460
- createdById: userId,
461
- },
453
+ const flashcardService = new FlashcardService(this.db);
454
+ const primarySet = await flashcardService.ensurePrimarySet(userId, input.workspaceId);
455
+ const orderOffset = primarySet.flashcards.reduce(
456
+ (max, card) => Math.max(max, card.order),
457
+ -1,
458
+ );
459
+ const flashcardTitle =
460
+ files.length === 1
461
+ ? `Flashcards - ${primaryFile.name}`
462
+ : `Flashcards - ${files.length} files`;
463
+
464
+ await this.db.artifact.update({
465
+ where: { id: primarySet.id },
466
+ data: { title: flashcardTitle },
462
467
  });
463
468
 
469
+ const appendCard = async (front: string, back: string, index: number) => {
470
+ await this.db.flashcard.create({
471
+ data: {
472
+ artifactId: primarySet.id,
473
+ front,
474
+ back,
475
+ order: orderOffset + 1 + index,
476
+ tags: ['ai-generated', 'medium'],
477
+ },
478
+ });
479
+ };
480
+
464
481
  try {
465
482
  const parsed = typeof content === 'string' ? JSON.parse(content) : content;
466
483
  const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
@@ -469,16 +486,7 @@ export class MediaAnalysisService extends BaseService {
469
486
  const card = flashcardData[i];
470
487
  const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
471
488
  const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
472
-
473
- await this.db.flashcard.create({
474
- data: {
475
- artifactId: artifact.id,
476
- front: front,
477
- back: back,
478
- order: i,
479
- tags: ['ai-generated', 'medium'],
480
- },
481
- });
489
+ await appendCard(front, back, i);
482
490
  }
483
491
  } catch (parseError) {
484
492
  console.error(
@@ -490,19 +498,16 @@ export class MediaAnalysisService extends BaseService {
490
498
  const line = lines[i];
491
499
  if (line.includes(' - ')) {
492
500
  const [front, back] = line.split(' - ');
493
- await this.db.flashcard.create({
494
- data: {
495
- artifactId: artifact.id,
496
- front: front.trim(),
497
- back: back.trim(),
498
- order: i,
499
- tags: ['ai-generated', 'medium'],
500
- },
501
- });
501
+ await appendCard(front.trim(), back.trim(), i);
502
502
  }
503
503
  }
504
504
  }
505
505
 
506
+ const artifact = await this.db.artifact.findUniqueOrThrow({
507
+ where: { id: primarySet.id },
508
+ include: { flashcards: true },
509
+ });
510
+
506
511
  results.artifacts.flashcards = artifact;
507
512
  await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
508
513
  await notifyArtifactReady(this.db, {
@@ -9,8 +9,8 @@ test('mergeWorksheetGenerationConfig: override beats preset and legacy', () => {
9
9
  { numQuestions: 3, difficulty: 'hard' },
10
10
  );
11
11
  assert.equal(r.mode, 'quiz');
12
- assert.equal(r.numQuestions, 10);
13
- assert.equal(r.difficulty, 'medium');
12
+ assert.equal(r.numQuestions, 3);
13
+ assert.equal(r.difficulty, 'hard');
14
14
  });
15
15
 
16
16
  test('normalizeWorksheetProblemForDb coerces MCQ answer to option index', () => {
@@ -1,11 +1,14 @@
1
1
  import type { PrismaClient } from '@prisma/client';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { BaseService } from '../base.service.js';
4
- import { ArtifactType } from '../../lib/constants.js';
5
4
  import { supabaseClient } from '../../lib/storage.js';
6
5
  import PusherService from '../../lib/pusher.js';
7
6
  import { ai } from '../../lib/ai/index.js';
8
7
  import { workspaceKbService } from './workspace-kb.service.js';
8
+ import {
9
+ getAccountSummary,
10
+ invalidateUserBillingCache,
11
+ } from '../billing/usage.service.js';
9
12
  import { getUserStorageLimit } from '../billing/subscription.service.js';
10
13
  import { notifyWorkspaceDeleted } from '../notifications/notification.service.js';
11
14
 
@@ -26,25 +29,41 @@ export class WorkspaceService extends BaseService {
26
29
  }
27
30
 
28
31
  async getTree(userId: string) {
29
- const allFolders = await this.db.folder.findMany({
30
- where: { ownerId: userId },
31
- orderBy: { updatedAt: 'desc' },
32
- });
33
-
34
- const allWorkspaces = await this.db.workspace.findMany({
35
- where: { ownerId: userId },
36
- include: {
37
- uploads: {
38
- select: {
39
- id: true,
40
- name: true,
41
- mimeType: true,
42
- createdAt: true,
32
+ const [allFolders, allWorkspaces] = await Promise.all([
33
+ this.db.folder.findMany({
34
+ where: { ownerId: userId },
35
+ select: {
36
+ id: true,
37
+ name: true,
38
+ parentId: true,
39
+ color: true,
40
+ markerColor: true,
41
+ updatedAt: true,
42
+ },
43
+ orderBy: { updatedAt: 'desc' },
44
+ }),
45
+ this.db.workspace.findMany({
46
+ where: { ownerId: userId },
47
+ select: {
48
+ id: true,
49
+ title: true,
50
+ folderId: true,
51
+ icon: true,
52
+ color: true,
53
+ markerColor: true,
54
+ updatedAt: true,
55
+ uploads: {
56
+ select: {
57
+ id: true,
58
+ name: true,
59
+ mimeType: true,
60
+ createdAt: true,
61
+ },
43
62
  },
44
63
  },
45
- },
46
- orderBy: { updatedAt: 'desc' },
47
- });
64
+ orderBy: { updatedAt: 'desc' },
65
+ }),
66
+ ]);
48
67
 
49
68
  return { folders: allFolders, workspaces: allWorkspaces };
50
69
  }
@@ -65,30 +84,19 @@ export class WorkspaceService extends BaseService {
65
84
  ownerId: userId,
66
85
  folderId: input.parentId ?? null,
67
86
  ...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
68
- artifacts: {
69
- create: {
70
- type: ArtifactType.FLASHCARD_SET,
71
- title: 'New Flashcard Set',
72
- },
73
- createMany: {
74
- data: [
75
- { type: ArtifactType.WORKSHEET, title: 'Worksheet 1' },
76
- { type: ArtifactType.WORKSHEET, title: 'Worksheet 2' },
77
- ],
78
- },
79
- },
80
87
  },
81
88
  });
82
89
 
83
- await ai.backend.initSession(ws.id, userId).catch((err) => {
90
+ void ai.backend.initSession(ws.id, userId).catch((err) => {
84
91
  this.logger.error('Failed to init AI session on workspace creation:', err);
85
92
  });
86
93
 
87
- await workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
94
+ void workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
88
95
  this.logger.error('Failed to create workspace knowledge base:', err);
89
96
  });
90
97
 
91
- await PusherService.emitLibraryUpdate(userId);
98
+ invalidateUserBillingCache(userId);
99
+ void PusherService.emitLibraryUpdate(userId);
92
100
 
93
101
  return ws;
94
102
  }
@@ -130,46 +138,45 @@ export class WorkspaceService extends BaseService {
130
138
  async get(userId: string, id: string) {
131
139
  const ws = await this.db.workspace.findFirst({
132
140
  where: { id, ownerId: userId },
133
- include: {
134
- artifacts: true,
135
- folder: true,
136
- uploads: true,
141
+ select: {
142
+ id: true,
143
+ title: true,
144
+ description: true,
145
+ icon: true,
146
+ color: true,
147
+ markerColor: true,
148
+ ownerId: true,
149
+ folderId: true,
150
+ fileBeingAnalyzed: true,
151
+ analysisProgress: true,
152
+ needsAnalysis: true,
153
+ createdAt: true,
154
+ updatedAt: true,
155
+ folder: { select: { id: true, name: true, color: true } },
156
+ uploads: {
157
+ select: {
158
+ id: true,
159
+ name: true,
160
+ mimeType: true,
161
+ size: true,
162
+ createdAt: true,
163
+ objectKey: true,
164
+ },
165
+ orderBy: { createdAt: 'desc' },
166
+ },
137
167
  },
138
168
  });
139
169
  if (!ws) throw new TRPCError({ code: 'NOT_FOUND' });
140
170
  return ws;
141
171
  }
142
172
 
143
- async getStats(userId: string) {
144
- const workspaces = await this.db.workspace.findMany({
145
- where: {
146
- OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
147
- },
148
- });
149
- const folders = await this.db.folder.findMany({
150
- where: { OR: [{ ownerId: userId }] },
151
- });
152
- const lastUpdated = await this.db.workspace.findFirst({
153
- where: {
154
- OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
155
- },
156
- orderBy: { updatedAt: 'desc' },
157
- });
158
-
159
- const spaceLeft = await this.db.fileAsset.aggregate({
160
- where: { workspaceId: { in: workspaces.map((ws: any) => ws.id) }, userId },
161
- _sum: { size: true },
162
- });
163
-
164
- const storageLimit = await getUserStorageLimit(userId);
173
+ async getAccountSummary(userId: string) {
174
+ return getAccountSummary(userId);
175
+ }
165
176
 
166
- return {
167
- workspaces: workspaces.length,
168
- folders: folders.length,
169
- lastUpdated: lastUpdated?.updatedAt,
170
- spaceUsed: spaceLeft._sum?.size ?? 0,
171
- spaceTotal: storageLimit,
172
- };
177
+ async getStats(userId: string) {
178
+ const summary = await getAccountSummary(userId);
179
+ return summary.stats;
173
180
  }
174
181
 
175
182
  async update(
package/src/trpc.ts CHANGED
@@ -95,12 +95,8 @@ const errorHandler = middleware(async ({ next }) => {
95
95
  /**
96
96
  * Middleware that enforces email verification
97
97
  */
98
- const isVerified = middleware(async ({ ctx, next }) => {
99
- const user = await ctx.db.user.findUnique({
100
- where: { id: (ctx.session as any).user.id },
101
- select: { emailVerified: true },
102
- });
103
-
98
+ const isVerified = middleware(({ ctx, next }) => {
99
+ const user = (ctx.session as { user?: { emailVerified?: boolean } })?.user;
104
100
  if (!user?.emailVerified) {
105
101
  throw new TRPCError({
106
102
  code: "FORBIDDEN",
@@ -168,13 +164,9 @@ const checkUsageLimit = middleware(async ({ ctx, next, path }) => {
168
164
  /**
169
165
  * Middleware that enforces system admin role
170
166
  */
171
- const isAdmin = middleware(async ({ ctx, next }) => {
172
- const user = await ctx.db.user.findUnique({
173
- where: { id: (ctx.session as any).user.id },
174
- include: { role: true },
175
- });
176
-
177
- if (user?.role?.name !== 'System Admin') {
167
+ const isAdmin = middleware(({ ctx, next }) => {
168
+ const user = (ctx.session as { user?: { isSystemAdmin?: boolean } })?.user;
169
+ if (!user?.isSystemAdmin) {
178
170
  throw new TRPCError({
179
171
  code: "FORBIDDEN",
180
172
  message: "You do not have permission to access this resource",