@goscribe/server 1.0.8 → 1.0.10

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 (58) hide show
  1. package/AUTH_FRONTEND_SPEC.md +21 -0
  2. package/CHAT_FRONTEND_SPEC.md +474 -0
  3. package/DATABASE_SETUP.md +165 -0
  4. package/MEETINGSUMMARY_FRONTEND_SPEC.md +28 -0
  5. package/PODCAST_FRONTEND_SPEC.md +595 -0
  6. package/STUDYGUIDE_FRONTEND_SPEC.md +18 -0
  7. package/WORKSHEETS_FRONTEND_SPEC.md +26 -0
  8. package/WORKSPACE_FRONTEND_SPEC.md +47 -0
  9. package/dist/lib/ai-session.d.ts +26 -0
  10. package/dist/lib/ai-session.js +343 -0
  11. package/dist/lib/inference.d.ts +2 -0
  12. package/dist/lib/inference.js +21 -0
  13. package/dist/lib/pusher.d.ts +14 -0
  14. package/dist/lib/pusher.js +94 -0
  15. package/dist/lib/storage.d.ts +10 -2
  16. package/dist/lib/storage.js +63 -6
  17. package/dist/routers/_app.d.ts +840 -58
  18. package/dist/routers/_app.js +6 -0
  19. package/dist/routers/ai-session.d.ts +0 -0
  20. package/dist/routers/ai-session.js +1 -0
  21. package/dist/routers/auth.d.ts +1 -0
  22. package/dist/routers/auth.js +6 -4
  23. package/dist/routers/chat.d.ts +171 -0
  24. package/dist/routers/chat.js +270 -0
  25. package/dist/routers/flashcards.d.ts +37 -0
  26. package/dist/routers/flashcards.js +128 -0
  27. package/dist/routers/meetingsummary.d.ts +0 -0
  28. package/dist/routers/meetingsummary.js +377 -0
  29. package/dist/routers/podcast.d.ts +277 -0
  30. package/dist/routers/podcast.js +847 -0
  31. package/dist/routers/studyguide.d.ts +54 -0
  32. package/dist/routers/studyguide.js +125 -0
  33. package/dist/routers/worksheets.d.ts +138 -51
  34. package/dist/routers/worksheets.js +317 -7
  35. package/dist/routers/workspace.d.ts +162 -7
  36. package/dist/routers/workspace.js +440 -8
  37. package/dist/server.js +6 -2
  38. package/package.json +11 -4
  39. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +213 -0
  40. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +31 -0
  41. package/prisma/migrations/migration_lock.toml +3 -0
  42. package/prisma/schema.prisma +87 -6
  43. package/prisma/seed.mjs +135 -0
  44. package/src/lib/ai-session.ts +412 -0
  45. package/src/lib/inference.ts +21 -0
  46. package/src/lib/pusher.ts +104 -0
  47. package/src/lib/storage.ts +89 -6
  48. package/src/routers/_app.ts +6 -0
  49. package/src/routers/auth.ts +8 -4
  50. package/src/routers/chat.ts +275 -0
  51. package/src/routers/flashcards.ts +142 -0
  52. package/src/routers/meetingsummary.ts +416 -0
  53. package/src/routers/podcast.ts +934 -0
  54. package/src/routers/studyguide.ts +144 -0
  55. package/src/routers/worksheets.ts +336 -7
  56. package/src/routers/workspace.ts +487 -8
  57. package/src/server.ts +7 -2
  58. package/test-ai-integration.js +134 -0
@@ -3,22 +3,69 @@ import { TRPCError } from '@trpc/server';
3
3
  import { router, authedProcedure } from '../trpc.js';
4
4
  import { bucket } from '../lib/storage.js';
5
5
  import { ArtifactType } from '@prisma/client';
6
+ import { aiSessionService } from '../lib/ai-session.js';
7
+ import PusherService from '../lib/pusher.js';
8
+ // Helper function to calculate search relevance score
9
+ function calculateRelevance(query, ...texts) {
10
+ const queryLower = query.toLowerCase();
11
+ let score = 0;
12
+ for (const text of texts) {
13
+ if (!text)
14
+ continue;
15
+ const textLower = text.toLowerCase();
16
+ // Exact match gets highest score
17
+ if (textLower.includes(queryLower)) {
18
+ score += 10;
19
+ }
20
+ // Word boundary matches get good score
21
+ const words = queryLower.split(/\s+/);
22
+ for (const word of words) {
23
+ if (word.length > 2 && textLower.includes(word)) {
24
+ score += 5;
25
+ }
26
+ }
27
+ // Partial matches get lower score
28
+ const queryChars = queryLower.split('');
29
+ let consecutiveMatches = 0;
30
+ for (const char of queryChars) {
31
+ if (textLower.includes(char)) {
32
+ consecutiveMatches++;
33
+ }
34
+ else {
35
+ consecutiveMatches = 0;
36
+ }
37
+ }
38
+ score += consecutiveMatches * 0.1;
39
+ }
40
+ return score;
41
+ }
6
42
  export const workspace = router({
7
43
  // List current user's workspaces
8
44
  list: authedProcedure
9
- .query(async ({ ctx }) => {
45
+ .input(z.object({
46
+ parentId: z.string().optional(),
47
+ }))
48
+ .query(async ({ ctx, input }) => {
10
49
  const workspaces = await ctx.db.workspace.findMany({
11
50
  where: {
12
51
  ownerId: ctx.session.user.id,
52
+ folderId: input.parentId ?? null,
13
53
  },
14
54
  orderBy: { updatedAt: 'desc' },
15
55
  });
16
- return workspaces;
56
+ const folders = await ctx.db.folder.findMany({
57
+ where: {
58
+ ownerId: ctx.session.user.id,
59
+ parentId: input.parentId ?? null,
60
+ },
61
+ });
62
+ return { workspaces, folders };
17
63
  }),
18
64
  create: authedProcedure
19
65
  .input(z.object({
20
66
  name: z.string().min(1).max(100),
21
67
  description: z.string().max(500).optional(),
68
+ parentId: z.string().optional(),
22
69
  }))
23
70
  .mutation(async ({ ctx, input }) => {
24
71
  const ws = await ctx.db.workspace.create({
@@ -26,6 +73,7 @@ export const workspace = router({
26
73
  title: input.name,
27
74
  description: input.description,
28
75
  ownerId: ctx.session.user.id,
76
+ folderId: input.parentId ?? null,
29
77
  artifacts: {
30
78
  create: {
31
79
  type: ArtifactType.FLASHCARD_SET,
@@ -42,21 +90,94 @@ export const workspace = router({
42
90
  });
43
91
  return ws;
44
92
  }),
93
+ createFolder: authedProcedure
94
+ .input(z.object({
95
+ name: z.string().min(1).max(100),
96
+ parentId: z.string().optional(),
97
+ }))
98
+ .mutation(async ({ ctx, input }) => {
99
+ const folder = await ctx.db.folder.create({
100
+ data: {
101
+ name: input.name,
102
+ ownerId: ctx.session.user.id,
103
+ parentId: input.parentId ?? null,
104
+ },
105
+ });
106
+ return folder;
107
+ }),
45
108
  get: authedProcedure
46
109
  .input(z.object({
47
- id: z.string().uuid(),
110
+ id: z.string(),
48
111
  }))
49
112
  .query(async ({ ctx, input }) => {
50
113
  const ws = await ctx.db.workspace.findFirst({
51
114
  where: { id: input.id, ownerId: ctx.session.user.id },
115
+ include: {
116
+ artifacts: true,
117
+ folder: true,
118
+ uploads: true,
119
+ },
52
120
  });
53
121
  if (!ws)
54
122
  throw new TRPCError({ code: 'NOT_FOUND' });
55
123
  return ws;
56
124
  }),
125
+ share: authedProcedure
126
+ .input(z.object({
127
+ id: z.string(),
128
+ }))
129
+ .query(async ({ ctx, input }) => {
130
+ const ws = await ctx.db.workspace.findFirst({
131
+ where: { id: input.id, ownerId: ctx.session.user.id },
132
+ });
133
+ if (!ws)
134
+ throw new TRPCError({ code: 'NOT_FOUND' });
135
+ // generate a unique share link if not exists
136
+ if (!ws.shareLink) {
137
+ const shareLink = [...Array(30)].map(() => (Math.random() * 36 | 0).toString(36)).join('');
138
+ const updated = await ctx.db.workspace.update({
139
+ where: { id: ws.id },
140
+ data: { shareLink },
141
+ });
142
+ return { shareLink: updated.shareLink };
143
+ }
144
+ }),
145
+ join: authedProcedure
146
+ .input(z.object({
147
+ shareLink: z.string().min(10).max(100),
148
+ }))
149
+ .mutation(async ({ ctx, input }) => {
150
+ const ws = await ctx.db.workspace.findFirst({
151
+ where: { shareLink: input.shareLink },
152
+ });
153
+ if (!ws)
154
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' });
155
+ if (ws.ownerId === ctx.session.user.id) {
156
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot join your own workspace' });
157
+ }
158
+ // add to sharedWith if not already
159
+ const alreadyShared = await ctx.db.workspace.findFirst({
160
+ where: { id: ws.id, sharedWith: { some: { id: ctx.session.user.id } } },
161
+ });
162
+ if (alreadyShared) {
163
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Already joined this workspace' });
164
+ }
165
+ await ctx.db.workspace.update({
166
+ where: { id: ws.id },
167
+ data: { sharedWith: { connect: { id: ctx.session.user.id } } },
168
+ });
169
+ return {
170
+ id: ws.id,
171
+ title: ws.title,
172
+ description: ws.description,
173
+ ownerId: ws.ownerId,
174
+ createdAt: ws.createdAt,
175
+ updatedAt: ws.updatedAt,
176
+ };
177
+ }),
57
178
  update: authedProcedure
58
179
  .input(z.object({
59
- id: z.string().uuid(),
180
+ id: z.string(),
60
181
  name: z.string().min(1).max(100).optional(),
61
182
  description: z.string().max(500).optional(),
62
183
  }))
@@ -77,7 +198,7 @@ export const workspace = router({
77
198
  }),
78
199
  delete: authedProcedure
79
200
  .input(z.object({
80
- id: z.string().uuid(),
201
+ id: z.string(),
81
202
  }))
82
203
  .mutation(async ({ ctx, input }) => {
83
204
  const deleted = await ctx.db.workspace.deleteMany({
@@ -87,9 +208,29 @@ export const workspace = router({
87
208
  throw new TRPCError({ code: 'NOT_FOUND' });
88
209
  return true;
89
210
  }),
211
+ getFolderInformation: authedProcedure
212
+ .input(z.object({
213
+ id: z.string(),
214
+ }))
215
+ .query(async ({ ctx, input }) => {
216
+ const folder = await ctx.db.folder.findFirst({ where: { id: input.id, ownerId: ctx.session.user.id } });
217
+ // find all of its parents
218
+ if (!folder)
219
+ throw new TRPCError({ code: 'NOT_FOUND' });
220
+ const parents = [];
221
+ let current = folder;
222
+ while (current.parentId) {
223
+ const parent = await ctx.db.folder.findFirst({ where: { id: current.parentId, ownerId: ctx.session.user.id } });
224
+ if (!parent)
225
+ break;
226
+ parents.push(parent);
227
+ current = parent;
228
+ }
229
+ return { folder, parents };
230
+ }),
90
231
  uploadFiles: authedProcedure
91
232
  .input(z.object({
92
- id: z.string().uuid(),
233
+ id: z.string(),
93
234
  files: z.array(z.object({
94
235
  filename: z.string().min(1).max(255),
95
236
  contentType: z.string().min(1).max(100),
@@ -138,8 +279,8 @@ export const workspace = router({
138
279
  }),
139
280
  deleteFiles: authedProcedure
140
281
  .input(z.object({
141
- fileId: z.array(z.string().uuid()),
142
- id: z.string().uuid(),
282
+ fileId: z.array(z.string()),
283
+ id: z.string(),
143
284
  }))
144
285
  .mutation(async ({ ctx, input }) => {
145
286
  // ensure files are in the user's workspace
@@ -168,4 +309,295 @@ export const workspace = router({
168
309
  });
169
310
  return true;
170
311
  }),
312
+ uploadAndAnalyzeMedia: authedProcedure
313
+ .input(z.object({
314
+ workspaceId: z.string(),
315
+ file: z.object({
316
+ filename: z.string(),
317
+ contentType: z.string(),
318
+ size: z.number(),
319
+ content: z.string(), // Base64 encoded file content
320
+ }),
321
+ generateStudyGuide: z.boolean().default(true),
322
+ generateFlashcards: z.boolean().default(true),
323
+ generateWorksheet: z.boolean().default(true),
324
+ }))
325
+ .mutation(async ({ ctx, input }) => {
326
+ console.log('🚀 uploadAndAnalyzeMedia started', {
327
+ workspaceId: input.workspaceId,
328
+ filename: input.file.filename,
329
+ fileSize: input.file.size,
330
+ generateStudyGuide: input.generateStudyGuide,
331
+ generateFlashcards: input.generateFlashcards,
332
+ generateWorksheet: input.generateWorksheet
333
+ });
334
+ // Verify workspace ownership
335
+ const workspace = await ctx.db.workspace.findFirst({
336
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id }
337
+ });
338
+ if (!workspace) {
339
+ console.error('❌ Workspace not found', { workspaceId: input.workspaceId, userId: ctx.session.user.id });
340
+ throw new TRPCError({ code: 'NOT_FOUND' });
341
+ }
342
+ console.log('✅ Workspace verified', { workspaceId: workspace.id, workspaceTitle: workspace.title });
343
+ // Convert base64 to buffer
344
+ console.log('📁 Converting base64 to buffer...');
345
+ const fileBuffer = Buffer.from(input.file.content, 'base64');
346
+ console.log('✅ File buffer created', { bufferSize: fileBuffer.length });
347
+ // // Check AI service health first
348
+ // console.log('🏥 Checking AI service health...');
349
+ // const isHealthy = await aiSessionService.checkHealth();
350
+ // if (!isHealthy) {
351
+ // console.error('❌ AI service is not available');
352
+ // await PusherService.emitError(input.workspaceId, 'AI service is currently unavailable');
353
+ // throw new TRPCError({
354
+ // code: 'SERVICE_UNAVAILABLE',
355
+ // message: 'AI service is currently unavailable. Please try again later.',
356
+ // });
357
+ // }
358
+ // console.log('✅ AI service is healthy');
359
+ // Initialize AI session
360
+ console.log('🤖 Initializing AI session...');
361
+ const session = await aiSessionService.initSession(input.workspaceId);
362
+ console.log('✅ AI session initialized', { sessionId: session.id });
363
+ const fileObj = new File([fileBuffer], input.file.filename, { type: input.file.contentType });
364
+ const fileType = input.file.contentType.startsWith('image/') ? 'image' : 'pdf';
365
+ console.log('📤 Uploading file to AI service...', { filename: input.file.filename, fileType });
366
+ await aiSessionService.uploadFile(session.id, fileObj, fileType);
367
+ console.log('✅ File uploaded to AI service');
368
+ console.log('🚀 Starting LLM session...');
369
+ try {
370
+ await aiSessionService.startLLMSession(session.id);
371
+ console.log('✅ LLM session started');
372
+ }
373
+ catch (error) {
374
+ console.error('❌ Failed to start LLM session:', error);
375
+ throw error;
376
+ }
377
+ // Analyze the file first
378
+ console.log('🔍 Analyzing file...', { fileType });
379
+ await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_start', { filename: input.file.filename, fileType });
380
+ try {
381
+ if (fileType === 'image') {
382
+ await aiSessionService.analyseImage(session.id);
383
+ console.log('✅ Image analysis completed');
384
+ }
385
+ else {
386
+ await aiSessionService.analysePDF(session.id);
387
+ console.log('✅ PDF analysis completed');
388
+ }
389
+ await PusherService.emitTaskComplete(input.workspaceId, 'file_analysis_complete', { filename: input.file.filename, fileType });
390
+ }
391
+ catch (error) {
392
+ console.error('❌ Failed to analyze file:', error);
393
+ await PusherService.emitError(input.workspaceId, `Failed to analyze ${fileType}: ${error}`, 'file_analysis');
394
+ throw error;
395
+ }
396
+ const results = {
397
+ filename: input.file.filename,
398
+ artifacts: {
399
+ studyGuide: null,
400
+ flashcards: null,
401
+ worksheet: null,
402
+ }
403
+ };
404
+ // Generate artifacts
405
+ if (input.generateStudyGuide) {
406
+ await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_load_start', { filename: input.file.filename });
407
+ const content = await aiSessionService.generateStudyGuide(session.id);
408
+ await PusherService.emitTaskComplete(input.workspaceId, 'study_guide_info', { contentLength: content.length });
409
+ let artifact = await ctx.db.artifact.findFirst({
410
+ where: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE },
411
+ });
412
+ if (!artifact) {
413
+ artifact = await ctx.db.artifact.create({
414
+ data: {
415
+ workspaceId: input.workspaceId,
416
+ type: ArtifactType.STUDY_GUIDE,
417
+ title: `Study Guide - ${input.file.filename}`,
418
+ createdById: ctx.session.user.id,
419
+ },
420
+ });
421
+ }
422
+ const lastVersion = await ctx.db.artifactVersion.findFirst({
423
+ where: { artifact: { workspaceId: input.workspaceId, type: ArtifactType.STUDY_GUIDE } },
424
+ orderBy: { version: 'desc' },
425
+ });
426
+ await ctx.db.artifactVersion.create({
427
+ data: { artifactId: artifact.id, version: lastVersion ? lastVersion.version + 1 : 1, content: content, createdById: ctx.session.user.id },
428
+ });
429
+ results.artifacts.studyGuide = artifact;
430
+ // Emit Pusher notification
431
+ await PusherService.emitStudyGuideComplete(input.workspaceId, artifact);
432
+ }
433
+ if (input.generateFlashcards) {
434
+ await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_load_start', { filename: input.file.filename });
435
+ const content = await aiSessionService.generateFlashcardQuestions(session.id, 10, 'medium');
436
+ await PusherService.emitTaskComplete(input.workspaceId, 'flash_card_info', { contentLength: content.length });
437
+ const artifact = await ctx.db.artifact.create({
438
+ data: {
439
+ workspaceId: input.workspaceId,
440
+ type: ArtifactType.FLASHCARD_SET,
441
+ title: `Flashcards - ${input.file.filename}`,
442
+ createdById: ctx.session.user.id,
443
+ },
444
+ });
445
+ // Parse JSON flashcard content
446
+ try {
447
+ const flashcardData = JSON.parse(content);
448
+ let createdCards = 0;
449
+ for (let i = 0; i < Math.min(flashcardData.length, 10); i++) {
450
+ const card = flashcardData[i];
451
+ const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
452
+ const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
453
+ await ctx.db.flashcard.create({
454
+ data: {
455
+ artifactId: artifact.id,
456
+ front: front,
457
+ back: back,
458
+ order: i,
459
+ tags: ['ai-generated', 'medium'],
460
+ },
461
+ });
462
+ createdCards++;
463
+ }
464
+ }
465
+ catch (parseError) {
466
+ // Fallback to text parsing if JSON fails
467
+ const lines = content.split('\n').filter(line => line.trim());
468
+ for (let i = 0; i < Math.min(lines.length, 10); i++) {
469
+ const line = lines[i];
470
+ if (line.includes(' - ')) {
471
+ const [front, back] = line.split(' - ');
472
+ await ctx.db.flashcard.create({
473
+ data: {
474
+ artifactId: artifact.id,
475
+ front: front.trim(),
476
+ back: back.trim(),
477
+ order: i,
478
+ tags: ['ai-generated', 'medium'],
479
+ },
480
+ });
481
+ }
482
+ }
483
+ }
484
+ results.artifacts.flashcards = artifact;
485
+ // Emit Pusher notification
486
+ await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
487
+ }
488
+ if (input.generateWorksheet) {
489
+ await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_load_start', { filename: input.file.filename });
490
+ const content = await aiSessionService.generateWorksheetQuestions(session.id, 8, 'medium');
491
+ await PusherService.emitTaskComplete(input.workspaceId, 'worksheet_info', { contentLength: content.length });
492
+ const artifact = await ctx.db.artifact.create({
493
+ data: {
494
+ workspaceId: input.workspaceId,
495
+ type: ArtifactType.WORKSHEET,
496
+ title: `Worksheet - ${input.file.filename}`,
497
+ createdById: ctx.session.user.id,
498
+ },
499
+ });
500
+ // Parse JSON worksheet content
501
+ try {
502
+ const worksheetData = JSON.parse(content);
503
+ // The actual worksheet data is in last_response as JSON
504
+ let actualWorksheetData = worksheetData;
505
+ if (worksheetData.last_response) {
506
+ try {
507
+ actualWorksheetData = JSON.parse(worksheetData.last_response);
508
+ }
509
+ catch (parseError) {
510
+ console.error('❌ Failed to parse last_response JSON:', parseError);
511
+ console.log('📋 Raw last_response:', worksheetData.last_response);
512
+ }
513
+ }
514
+ // Handle different JSON structures
515
+ const problems = actualWorksheetData.problems || actualWorksheetData.questions || actualWorksheetData || [];
516
+ let createdQuestions = 0;
517
+ for (let i = 0; i < Math.min(problems.length, 8); i++) {
518
+ const problem = problems[i];
519
+ const prompt = problem.question || problem.prompt || `Question ${i + 1}`;
520
+ const answer = problem.answer || problem.solution || `Answer ${i + 1}`;
521
+ const type = problem.type || 'TEXT';
522
+ const options = problem.options || [];
523
+ await ctx.db.worksheetQuestion.create({
524
+ data: {
525
+ artifactId: artifact.id,
526
+ prompt: prompt,
527
+ answer: answer,
528
+ difficulty: 'MEDIUM',
529
+ order: i,
530
+ meta: {
531
+ type: type,
532
+ options: options.length > 0 ? options : undefined
533
+ },
534
+ },
535
+ });
536
+ createdQuestions++;
537
+ }
538
+ }
539
+ catch (parseError) {
540
+ console.error('❌ Failed to parse worksheet JSON, using fallback parsing:', parseError);
541
+ // Fallback to text parsing if JSON fails
542
+ const lines = content.split('\n').filter(line => line.trim());
543
+ for (let i = 0; i < Math.min(lines.length, 8); i++) {
544
+ const line = lines[i];
545
+ if (line.includes(' - ')) {
546
+ const [prompt, answer] = line.split(' - ');
547
+ await ctx.db.worksheetQuestion.create({
548
+ data: {
549
+ artifactId: artifact.id,
550
+ prompt: prompt.trim(),
551
+ answer: answer.trim(),
552
+ difficulty: 'MEDIUM',
553
+ order: i,
554
+ meta: { type: 'TEXT', },
555
+ },
556
+ });
557
+ }
558
+ }
559
+ }
560
+ results.artifacts.worksheet = artifact;
561
+ // Emit Pusher notification
562
+ await PusherService.emitWorksheetComplete(input.workspaceId, artifact);
563
+ }
564
+ await PusherService.emitTaskComplete(input.workspaceId, 'analysis_cleanup_start', { filename: input.file.filename });
565
+ aiSessionService.deleteSession(session.id);
566
+ await PusherService.emitTaskComplete(input.workspaceId, 'analysis_cleanup_complete', { filename: input.file.filename });
567
+ // Emit overall completion notification
568
+ await PusherService.emitOverallComplete(input.workspaceId, input.file.filename, results.artifacts);
569
+ return results;
570
+ }),
571
+ search: authedProcedure
572
+ .input(z.object({
573
+ query: z.string(),
574
+ limit: z.number().min(1).max(100).default(20),
575
+ }))
576
+ .query(async ({ ctx, input }) => {
577
+ const { query } = input;
578
+ const workspaces = await ctx.db.workspace.findMany({
579
+ where: {
580
+ ownerId: ctx.session.user.id,
581
+ OR: [
582
+ {
583
+ title: {
584
+ contains: query,
585
+ mode: 'insensitive',
586
+ },
587
+ },
588
+ {
589
+ description: {
590
+ contains: query,
591
+ mode: 'insensitive',
592
+ },
593
+ },
594
+ ],
595
+ },
596
+ orderBy: {
597
+ updatedAt: 'desc',
598
+ },
599
+ take: input.limit,
600
+ });
601
+ return workspaces;
602
+ })
171
603
  });
package/dist/server.js CHANGED
@@ -13,12 +13,16 @@ async function main() {
13
13
  // Middlewares
14
14
  app.use(helmet());
15
15
  app.use(cors({
16
- origin: "http://localhost:3000", // your Next.js dev URL
16
+ origin: process.env.FRONTEND_URL || "http://localhost:3000",
17
17
  credentials: true, // allow cookies
18
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
19
+ allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'Set-Cookie'],
20
+ exposedHeaders: ['Set-Cookie'],
18
21
  }));
19
22
  app.use(morgan('dev'));
20
23
  app.use(compression());
21
- app.use(express.json());
24
+ app.use(express.json({ limit: '50mb' }));
25
+ app.use(express.urlencoded({ limit: '50mb', extended: true }));
22
26
  // Health (plain Express)
23
27
  app.get('/', (_req, res) => {
24
28
  res.json({ ok: true, service: 'trpc-express', ts: Date.now() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,9 +12,10 @@
12
12
  }
13
13
  },
14
14
  "scripts": {
15
- "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
15
+ "dev": "tsx watch src/server.ts",
16
16
  "build": "npx prisma generate && tsc -p .",
17
- "start": "node --experimental-specifier-resolution=node dist/server.js"
17
+ "start": "node --experimental-specifier-resolution=node dist/server.js",
18
+ "seed": "node --experimental-specifier-resolution=node prisma/seed.mjs"
18
19
  },
19
20
  "author": "",
20
21
  "license": "MIT",
@@ -22,9 +23,11 @@
22
23
  "@auth/express": "^0.11.0",
23
24
  "@auth/prisma-adapter": "^2.10.0",
24
25
  "@google-cloud/storage": "^7.17.0",
25
- "@goscribe/server": "^1.0.7",
26
+ "@goscribe/server": "^1.0.8",
26
27
  "@prisma/client": "^6.14.0",
27
28
  "@trpc/server": "^11.5.0",
29
+ "@types/uuid": "^10.0.0",
30
+ "@vingeray/editorjs-markdown-converter": "^0.1.2",
28
31
  "bcryptjs": "^3.0.2",
29
32
  "compression": "^1.8.1",
30
33
  "cookie": "^1.0.2",
@@ -34,6 +37,9 @@
34
37
  "helmet": "^8.1.0",
35
38
  "morgan": "^1.10.1",
36
39
  "prisma": "^6.14.0",
40
+ "pusher": "^5.2.0",
41
+ "pusher-js": "^8.4.0",
42
+ "socket.io": "^4.8.1",
37
43
  "superjson": "^2.2.2",
38
44
  "zod": "^4.1.1"
39
45
  },
@@ -48,6 +54,7 @@
48
54
  "ts-node-dev": "^2.0.0",
49
55
  "tsc-alias": "^1.8.16",
50
56
  "tsc-esm-fix": "^3.1.2",
57
+ "tsx": "^4.20.6",
51
58
  "typescript": "^5.9.2",
52
59
  "typescript-transform-paths": "^3.5.5"
53
60
  }