@goscribe/server 1.1.7 → 1.3.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.
- package/.env.example +43 -0
- package/check-difficulty.cjs +14 -0
- package/check-questions.cjs +14 -0
- package/db-summary.cjs +22 -0
- package/dist/routers/auth.js +1 -1
- package/mcq-test.cjs +36 -0
- package/package.json +10 -2
- package/prisma/migrations/20260413143206_init/migration.sql +873 -0
- package/prisma/schema.prisma +485 -292
- package/src/context.ts +4 -1
- package/src/lib/activity_human_description.test.ts +28 -0
- package/src/lib/activity_human_description.ts +239 -0
- package/src/lib/activity_log_service.test.ts +37 -0
- package/src/lib/activity_log_service.ts +353 -0
- package/src/lib/ai-session.ts +194 -112
- package/src/lib/constants.ts +14 -0
- package/src/lib/email.ts +230 -0
- package/src/lib/env.ts +23 -6
- package/src/lib/inference.ts +3 -3
- package/src/lib/logger.ts +26 -9
- package/src/lib/notification-service.test.ts +106 -0
- package/src/lib/notification-service.ts +677 -0
- package/src/lib/prisma.ts +6 -1
- package/src/lib/pusher.ts +90 -6
- package/src/lib/retry.ts +61 -0
- package/src/lib/storage.ts +2 -2
- package/src/lib/stripe.ts +39 -0
- package/src/lib/subscription_service.ts +722 -0
- package/src/lib/usage_service.ts +74 -0
- package/src/lib/worksheet-generation.test.ts +31 -0
- package/src/lib/worksheet-generation.ts +139 -0
- package/src/lib/workspace-access.ts +13 -0
- package/src/routers/_app.ts +11 -0
- package/src/routers/admin.ts +710 -0
- package/src/routers/annotations.ts +227 -0
- package/src/routers/auth.ts +432 -33
- package/src/routers/copilot.ts +719 -0
- package/src/routers/flashcards.ts +207 -80
- package/src/routers/members.ts +280 -80
- package/src/routers/notifications.ts +142 -0
- package/src/routers/payment.ts +448 -0
- package/src/routers/podcast.ts +133 -108
- package/src/routers/studyguide.ts +80 -74
- package/src/routers/worksheets.ts +300 -80
- package/src/routers/workspace.ts +538 -328
- package/src/scripts/purge-deleted-users.ts +167 -0
- package/src/server.ts +140 -12
- package/src/services/flashcard-progress.service.ts +52 -43
- package/src/trpc.ts +184 -5
- package/test-generate.js +30 -0
- package/test-ratio.cjs +9 -0
- package/zod-test.cjs +22 -0
- package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
- package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
- package/prisma/seed.mjs +0 -135
- package/src/routers/meetingsummary.ts +0 -416
package/prisma/schema.prisma
CHANGED
|
@@ -5,11 +5,11 @@ generator client {
|
|
|
5
5
|
datasource db {
|
|
6
6
|
provider = "postgresql"
|
|
7
7
|
url = env("DATABASE_URL")
|
|
8
|
-
directUrl = env("DIRECT_URL")
|
|
8
|
+
directUrl = env("DIRECT_URL")
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
//
|
|
12
|
-
// Enums
|
|
12
|
+
// Enums (match prisma1/migrations enum DDL)
|
|
13
13
|
//
|
|
14
14
|
enum ArtifactType {
|
|
15
15
|
STUDY_GUIDE
|
|
@@ -17,6 +17,7 @@ enum ArtifactType {
|
|
|
17
17
|
WORKSHEET
|
|
18
18
|
MEETING_SUMMARY
|
|
19
19
|
PODCAST_EPISODE
|
|
20
|
+
STORAGE
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
enum Difficulty {
|
|
@@ -34,56 +35,132 @@ enum QuestionType {
|
|
|
34
35
|
FILL_IN_THE_BLANK
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
enum CopilotMessageRole {
|
|
39
|
+
USER
|
|
40
|
+
ASSISTANT
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
enum NotificationPriority {
|
|
44
|
+
LOW
|
|
45
|
+
NORMAL
|
|
46
|
+
HIGH
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
enum InvoiceType {
|
|
50
|
+
SUBSCRIPTION
|
|
51
|
+
TOPUP
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
enum ActivityLogCategory {
|
|
55
|
+
AUTH
|
|
56
|
+
WORKSPACE
|
|
57
|
+
BILLING
|
|
58
|
+
ADMIN
|
|
59
|
+
CONTENT
|
|
60
|
+
SYSTEM
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
enum ActivityLogStatus {
|
|
64
|
+
SUCCESS
|
|
65
|
+
FAILURE
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
//
|
|
38
69
|
// NextAuth-compatible auth models (minimal)
|
|
39
70
|
//
|
|
40
71
|
model User {
|
|
41
|
-
id
|
|
42
|
-
name
|
|
43
|
-
email
|
|
44
|
-
emailVerified
|
|
45
|
-
passwordHash
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
versions ArtifactVersion[] @relation("UserArtifactVersions")
|
|
57
|
-
|
|
58
|
-
// Progress tracking
|
|
72
|
+
id String @id @default(cuid())
|
|
73
|
+
name String?
|
|
74
|
+
email String? @unique
|
|
75
|
+
emailVerified DateTime?
|
|
76
|
+
passwordHash String?
|
|
77
|
+
createdAt DateTime @default(now())
|
|
78
|
+
updatedAt DateTime @updatedAt
|
|
79
|
+
fileAssetId String?
|
|
80
|
+
roleId String?
|
|
81
|
+
deletedAt DateTime?
|
|
82
|
+
stripe_customer_id String? @unique
|
|
83
|
+
artifacts Artifact[] @relation("UserArtifacts")
|
|
84
|
+
versions ArtifactVersion[] @relation("UserArtifactVersions")
|
|
85
|
+
chats Chat[]
|
|
86
|
+
uploads FileAsset[] @relation("UserUploads")
|
|
59
87
|
flashcardProgress FlashcardProgress[] @relation("UserFlashcardProgress")
|
|
88
|
+
folders Folder[] @relation("UserFolders")
|
|
89
|
+
workspaces Workspace[] @relation("UserWorkspaces")
|
|
90
|
+
workspaceMemberships WorkspaceMember[]
|
|
91
|
+
invitedInWorkspaces Workspace[] @relation("WorkspaceSharedWith")
|
|
92
|
+
notifications Notification[]
|
|
93
|
+
actorNotifications Notification[] @relation("NotificationActor")
|
|
94
|
+
session Session[]
|
|
95
|
+
studyGuideComments StudyGuideComment[] @relation("UserStudyGuideComments")
|
|
96
|
+
highlights StudyGuideHighlight[] @relation("UserHighlights")
|
|
97
|
+
profilePicture FileAsset? @relation(fields: [fileAssetId], references: [id])
|
|
98
|
+
role Role? @relation(fields: [roleId], references: [id])
|
|
60
99
|
worksheetQuestionProgress WorksheetQuestionProgress[]
|
|
100
|
+
worksheetPresets WorksheetPreset[]
|
|
101
|
+
copilotConversations CopilotConversation[]
|
|
102
|
+
sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
|
|
103
|
+
subscriptions Subscription[]
|
|
104
|
+
invoices Invoice[]
|
|
105
|
+
credits UserCredit[]
|
|
106
|
+
idempotencyRecords IdempotencyRecord[]
|
|
107
|
+
activityLogs ActivityLog[] @relation("ActivityLogActor")
|
|
108
|
+
passwordResetTokens PasswordResetToken[]
|
|
109
|
+
}
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
|
|
111
|
+
/// One-time tokens for forgot-password flow (token stored as SHA-256 hash of the secret from the email link).
|
|
112
|
+
model PasswordResetToken {
|
|
113
|
+
id String @id @default(cuid())
|
|
114
|
+
userId String
|
|
115
|
+
tokenHash String @unique
|
|
116
|
+
expiresAt DateTime
|
|
117
|
+
usedAt DateTime?
|
|
118
|
+
createdAt DateTime @default(now())
|
|
119
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
64
120
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
121
|
+
@@index([userId])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
model Role {
|
|
125
|
+
id String @id @default(cuid())
|
|
126
|
+
name String @unique
|
|
127
|
+
users User[]
|
|
70
128
|
}
|
|
71
129
|
|
|
72
130
|
model Notification {
|
|
73
|
-
id
|
|
74
|
-
userId
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
131
|
+
id String @id @default(cuid())
|
|
132
|
+
userId String
|
|
133
|
+
actorUserId String?
|
|
134
|
+
workspaceId String?
|
|
135
|
+
type String
|
|
136
|
+
title String
|
|
137
|
+
body String
|
|
138
|
+
content String?
|
|
139
|
+
actionUrl String?
|
|
140
|
+
metadata Json?
|
|
141
|
+
priority NotificationPriority @default(NORMAL)
|
|
142
|
+
sourceId String?
|
|
143
|
+
read Boolean @default(false)
|
|
144
|
+
readAt DateTime?
|
|
145
|
+
deliveredAt DateTime?
|
|
146
|
+
createdAt DateTime @default(now())
|
|
147
|
+
updatedAt DateTime @updatedAt
|
|
148
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
149
|
+
actor User? @relation("NotificationActor", fields: [actorUserId], references: [id], onDelete: SetNull)
|
|
150
|
+
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: SetNull)
|
|
151
|
+
|
|
152
|
+
@@index([userId, read, createdAt(sort: Desc)])
|
|
153
|
+
@@index([userId, createdAt(sort: Desc)])
|
|
154
|
+
@@index([type, createdAt(sort: Desc)])
|
|
155
|
+
@@index([workspaceId])
|
|
156
|
+
@@unique([userId, type, sourceId])
|
|
80
157
|
}
|
|
81
158
|
|
|
82
159
|
model Session {
|
|
83
160
|
id String @id @default(cuid())
|
|
84
161
|
userId String
|
|
85
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
86
162
|
expires DateTime
|
|
163
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
87
164
|
}
|
|
88
165
|
|
|
89
166
|
model VerificationToken {
|
|
@@ -94,66 +171,49 @@ model VerificationToken {
|
|
|
94
171
|
@@unique([identifier, token])
|
|
95
172
|
}
|
|
96
173
|
|
|
97
|
-
//
|
|
98
|
-
// Filesystem-like structure
|
|
99
|
-
//
|
|
100
174
|
model Folder {
|
|
101
|
-
id
|
|
102
|
-
name
|
|
103
|
-
ownerId
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
workspaces Workspace[]
|
|
114
|
-
|
|
115
|
-
// Metadata
|
|
116
|
-
createdAt DateTime @default(now())
|
|
117
|
-
updatedAt DateTime @updatedAt
|
|
175
|
+
id String @id @default(cuid())
|
|
176
|
+
name String
|
|
177
|
+
ownerId String
|
|
178
|
+
parentId String?
|
|
179
|
+
createdAt DateTime @default(now())
|
|
180
|
+
updatedAt DateTime @updatedAt
|
|
181
|
+
color String @default("#9D00FF")
|
|
182
|
+
markerColor String?
|
|
183
|
+
owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
184
|
+
parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
|
|
185
|
+
children Folder[] @relation("FolderChildren")
|
|
186
|
+
workspaces Workspace[]
|
|
118
187
|
|
|
119
|
-
// Helpful composite index: folders per owner + parent
|
|
120
188
|
@@index([ownerId, parentId])
|
|
121
189
|
}
|
|
122
190
|
|
|
123
191
|
model Workspace {
|
|
124
|
-
id
|
|
125
|
-
title
|
|
126
|
-
description
|
|
127
|
-
ownerId
|
|
128
|
-
owner
|
|
129
|
-
icon
|
|
130
|
-
color
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
folderId
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// AI outputs for this workspace (study guides, flashcards, etc.)
|
|
150
|
-
artifacts Artifact[]
|
|
151
|
-
|
|
152
|
-
// Invitations
|
|
153
|
-
invitations WorkspaceInvitation[]
|
|
154
|
-
|
|
155
|
-
createdAt DateTime @default(now())
|
|
156
|
-
updatedAt DateTime @updatedAt
|
|
192
|
+
id String @id @default(cuid())
|
|
193
|
+
title String
|
|
194
|
+
description String?
|
|
195
|
+
ownerId String
|
|
196
|
+
owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
197
|
+
icon String @default("📄")
|
|
198
|
+
color String @default("#9D00FF")
|
|
199
|
+
markerColor String?
|
|
200
|
+
folderId String?
|
|
201
|
+
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
|
202
|
+
channels Channel[]
|
|
203
|
+
copilotConversations CopilotConversation[]
|
|
204
|
+
sharedWith User[] @relation("WorkspaceSharedWith")
|
|
205
|
+
members WorkspaceMember[]
|
|
206
|
+
fileBeingAnalyzed Boolean @default(false)
|
|
207
|
+
analysisProgress Json?
|
|
208
|
+
needsAnalysis Boolean @default(false)
|
|
209
|
+
uploads FileAsset[]
|
|
210
|
+
artifacts Artifact[]
|
|
211
|
+
worksheetPresets WorksheetPreset[]
|
|
212
|
+
invitations WorkspaceInvitation[]
|
|
213
|
+
notifications Notification[]
|
|
214
|
+
activityLogs ActivityLog[]
|
|
215
|
+
createdAt DateTime @default(now())
|
|
216
|
+
updatedAt DateTime @updatedAt
|
|
157
217
|
|
|
158
218
|
@@index([ownerId, folderId])
|
|
159
219
|
}
|
|
@@ -161,302 +221,435 @@ model Workspace {
|
|
|
161
221
|
model Channel {
|
|
162
222
|
id String @id @default(cuid())
|
|
163
223
|
workspaceId String
|
|
224
|
+
name String
|
|
225
|
+
createdAt DateTime @default(now())
|
|
164
226
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
165
|
-
|
|
166
|
-
name String
|
|
167
|
-
|
|
168
|
-
createdAt DateTime @default(now())
|
|
169
|
-
chats Chat[]
|
|
227
|
+
chats Chat[]
|
|
170
228
|
|
|
171
229
|
@@index([workspaceId])
|
|
172
230
|
}
|
|
173
231
|
|
|
174
232
|
model Chat {
|
|
175
|
-
id String
|
|
233
|
+
id String @id @default(cuid())
|
|
176
234
|
channelId String
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
180
|
-
userId String?
|
|
181
|
-
message String // chat message content
|
|
182
|
-
|
|
235
|
+
userId String?
|
|
236
|
+
message String
|
|
183
237
|
updatedAt DateTime @updatedAt
|
|
184
238
|
createdAt DateTime @default(now())
|
|
239
|
+
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
|
240
|
+
user User? @relation(fields: [userId], references: [id])
|
|
185
241
|
|
|
186
242
|
@@index([channelId, createdAt])
|
|
187
243
|
}
|
|
188
244
|
|
|
245
|
+
model CopilotConversation {
|
|
246
|
+
id String @id @default(cuid())
|
|
247
|
+
workspaceId String
|
|
248
|
+
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
249
|
+
userId String
|
|
250
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
251
|
+
title String @default("New Chat")
|
|
252
|
+
messages CopilotMessage[]
|
|
253
|
+
createdAt DateTime @default(now())
|
|
254
|
+
updatedAt DateTime @updatedAt
|
|
255
|
+
|
|
256
|
+
@@index([workspaceId, userId, updatedAt])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
model CopilotMessage {
|
|
260
|
+
id String @id @default(cuid())
|
|
261
|
+
conversationId String
|
|
262
|
+
conversation CopilotConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
263
|
+
role CopilotMessageRole
|
|
264
|
+
content String
|
|
265
|
+
createdAt DateTime @default(now())
|
|
266
|
+
|
|
267
|
+
@@index([conversationId, createdAt])
|
|
268
|
+
}
|
|
269
|
+
|
|
189
270
|
//
|
|
190
271
|
// User uploads (source materials for AI)
|
|
191
272
|
//
|
|
192
273
|
model FileAsset {
|
|
193
|
-
id
|
|
194
|
-
workspaceId
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
userId String?
|
|
198
|
-
user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
|
|
199
|
-
|
|
274
|
+
id String @id @default(cuid())
|
|
275
|
+
workspaceId String?
|
|
276
|
+
userId String?
|
|
200
277
|
name String
|
|
201
278
|
mimeType String
|
|
202
279
|
size Int
|
|
203
280
|
bucket String?
|
|
204
281
|
objectKey String?
|
|
205
|
-
url String?
|
|
206
|
-
checksum String?
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
User
|
|
282
|
+
url String?
|
|
283
|
+
checksum String?
|
|
284
|
+
meta Json?
|
|
285
|
+
createdAt DateTime @default(now())
|
|
286
|
+
aiTranscription Json? @default("{}")
|
|
287
|
+
user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
|
|
288
|
+
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
289
|
+
User User[]
|
|
213
290
|
|
|
214
291
|
@@index([workspaceId])
|
|
215
292
|
@@index([userId, createdAt])
|
|
216
293
|
}
|
|
217
294
|
|
|
218
|
-
//
|
|
219
|
-
// AI Outputs (Artifacts) with Versioning
|
|
220
|
-
// - One Artifact per output stream (e.g., one Study Guide, with many versions)
|
|
221
|
-
// - Some artifact types (flashcards, worksheet) have child rows
|
|
222
|
-
//
|
|
223
295
|
model Artifact {
|
|
224
|
-
id
|
|
225
|
-
workspaceId
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
296
|
+
id String @id @default(cuid())
|
|
297
|
+
workspaceId String
|
|
298
|
+
type ArtifactType
|
|
299
|
+
title String
|
|
300
|
+
isArchived Boolean @default(false)
|
|
301
|
+
difficulty Difficulty?
|
|
302
|
+
estimatedTime String?
|
|
303
|
+
createdById String?
|
|
304
|
+
createdAt DateTime @default(now())
|
|
305
|
+
updatedAt DateTime @updatedAt
|
|
306
|
+
description String?
|
|
307
|
+
generating Boolean @default(false)
|
|
233
308
|
generatingMetadata Json?
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
createdById String?
|
|
243
|
-
createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id], onDelete: SetNull)
|
|
244
|
-
|
|
245
|
-
versions ArtifactVersion[] // text/transcript versions etc.
|
|
246
|
-
flashcards Flashcard[] // only meaningful for FLASHCARD_SET
|
|
247
|
-
questions WorksheetQuestion[] // only meaningful for WORKSHEET
|
|
248
|
-
podcastSegments PodcastSegment[] // only meaningful for PODCAST_EPISODE
|
|
249
|
-
|
|
250
|
-
createdAt DateTime @default(now())
|
|
251
|
-
updatedAt DateTime @updatedAt
|
|
309
|
+
worksheetConfig Json?
|
|
310
|
+
imageObjectKey String?
|
|
311
|
+
createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id])
|
|
312
|
+
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
313
|
+
versions ArtifactVersion[]
|
|
314
|
+
flashcards Flashcard[]
|
|
315
|
+
podcastSegments PodcastSegment[]
|
|
316
|
+
questions WorksheetQuestion[]
|
|
252
317
|
|
|
253
318
|
@@index([workspaceId, type])
|
|
254
319
|
}
|
|
255
320
|
|
|
256
321
|
model ArtifactVersion {
|
|
257
|
-
id
|
|
258
|
-
artifactId
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
content String // rich text serialized as markdown/HTML stored as TEXT
|
|
263
|
-
|
|
264
|
-
// For Podcast episodes or other media, store URLs / durations / etc. in data
|
|
265
|
-
data Json? // e.g., { "audioUrl": "...", "durationSec": 312, "voice": "..." }
|
|
266
|
-
|
|
267
|
-
// Version sequencing (auto-increment per artifact)
|
|
268
|
-
version Int
|
|
269
|
-
|
|
322
|
+
id String @id @default(cuid())
|
|
323
|
+
artifactId String
|
|
324
|
+
content String
|
|
325
|
+
data Json?
|
|
326
|
+
version Int
|
|
270
327
|
createdById String?
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
328
|
+
createdAt DateTime @default(now())
|
|
329
|
+
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
330
|
+
createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id])
|
|
331
|
+
highlights StudyGuideHighlight[]
|
|
274
332
|
|
|
275
|
-
@@unique([artifactId, version])
|
|
333
|
+
@@unique([artifactId, version])
|
|
276
334
|
@@index([artifactId])
|
|
277
335
|
}
|
|
278
336
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
337
|
+
model StudyGuideHighlight {
|
|
338
|
+
id String @id @default(cuid())
|
|
339
|
+
artifactVersionId String
|
|
340
|
+
userId String
|
|
341
|
+
startOffset Int
|
|
342
|
+
endOffset Int
|
|
343
|
+
selectedText String
|
|
344
|
+
color String @default("#FBBF24")
|
|
345
|
+
createdAt DateTime @default(now())
|
|
346
|
+
updatedAt DateTime @updatedAt
|
|
347
|
+
comments StudyGuideComment[]
|
|
348
|
+
artifactVersion ArtifactVersion @relation(fields: [artifactVersionId], references: [id], onDelete: Cascade)
|
|
349
|
+
user User @relation("UserHighlights", fields: [userId], references: [id], onDelete: Cascade)
|
|
350
|
+
|
|
351
|
+
@@index([artifactVersionId, userId])
|
|
352
|
+
}
|
|
291
353
|
|
|
292
|
-
|
|
293
|
-
|
|
354
|
+
model StudyGuideComment {
|
|
355
|
+
id String @id @default(cuid())
|
|
356
|
+
highlightId String
|
|
357
|
+
userId String
|
|
358
|
+
content String
|
|
359
|
+
createdAt DateTime @default(now())
|
|
360
|
+
updatedAt DateTime @updatedAt
|
|
361
|
+
highlight StudyGuideHighlight @relation(fields: [highlightId], references: [id], onDelete: Cascade)
|
|
362
|
+
user User @relation("UserStudyGuideComments", fields: [userId], references: [id], onDelete: Cascade)
|
|
363
|
+
|
|
364
|
+
@@index([highlightId])
|
|
365
|
+
}
|
|
294
366
|
|
|
295
|
-
|
|
367
|
+
model Flashcard {
|
|
368
|
+
id String @id @default(cuid())
|
|
369
|
+
artifactId String
|
|
370
|
+
front String
|
|
371
|
+
back String
|
|
372
|
+
tags String[]
|
|
373
|
+
order Int @default(0)
|
|
374
|
+
createdAt DateTime @default(now())
|
|
375
|
+
acceptedAnswers String[] @default([])
|
|
376
|
+
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
377
|
+
progress FlashcardProgress[]
|
|
296
378
|
|
|
297
379
|
@@index([artifactId])
|
|
298
380
|
}
|
|
299
381
|
|
|
300
|
-
//
|
|
301
|
-
// User Progress on Flashcards (spaced repetition, mastery tracking)
|
|
302
|
-
//
|
|
303
382
|
model FlashcardProgress {
|
|
304
|
-
id
|
|
305
|
-
userId
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// Mastery level (0-100)
|
|
323
|
-
masteryLevel Int @default(0)
|
|
324
|
-
|
|
325
|
-
// Timestamps
|
|
326
|
-
lastStudiedAt DateTime?
|
|
327
|
-
nextReviewAt DateTime?
|
|
328
|
-
|
|
329
|
-
createdAt DateTime @default(now())
|
|
330
|
-
updatedAt DateTime @updatedAt
|
|
383
|
+
id String @id @default(cuid())
|
|
384
|
+
userId String
|
|
385
|
+
flashcardId String
|
|
386
|
+
timesStudied Int @default(0)
|
|
387
|
+
timesCorrect Int @default(0)
|
|
388
|
+
timesIncorrect Int @default(0)
|
|
389
|
+
timesIncorrectConsecutive Int @default(0)
|
|
390
|
+
easeFactor Float @default(2.5)
|
|
391
|
+
interval Int @default(0)
|
|
392
|
+
repetitions Int @default(0)
|
|
393
|
+
masteryLevel Int @default(0)
|
|
394
|
+
lastStudiedAt DateTime?
|
|
395
|
+
nextReviewAt DateTime?
|
|
396
|
+
createdAt DateTime @default(now())
|
|
397
|
+
updatedAt DateTime @updatedAt
|
|
398
|
+
flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
|
|
399
|
+
user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
|
|
331
400
|
|
|
332
401
|
@@unique([userId, flashcardId])
|
|
333
402
|
@@index([userId, nextReviewAt])
|
|
334
403
|
@@index([flashcardId])
|
|
335
404
|
}
|
|
336
405
|
|
|
337
|
-
//
|
|
338
|
-
// Worksheet Questions (child items of a WORKSHEET Artifact)
|
|
339
|
-
//
|
|
340
406
|
model WorksheetQuestion {
|
|
341
|
-
id String
|
|
407
|
+
id String @id @default(cuid())
|
|
342
408
|
artifactId String
|
|
343
|
-
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
344
|
-
|
|
345
409
|
prompt String
|
|
346
410
|
answer String?
|
|
347
|
-
type QuestionType
|
|
348
|
-
difficulty Difficulty
|
|
349
|
-
order Int
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
progress WorksheetQuestionProgress[]
|
|
411
|
+
type QuestionType @default(TEXT)
|
|
412
|
+
difficulty Difficulty @default(MEDIUM)
|
|
413
|
+
order Int @default(0)
|
|
414
|
+
meta Json?
|
|
415
|
+
createdAt DateTime @default(now())
|
|
416
|
+
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
417
|
+
progress WorksheetQuestionProgress[]
|
|
355
418
|
|
|
356
419
|
@@index([artifactId])
|
|
357
420
|
}
|
|
358
421
|
|
|
359
|
-
//
|
|
360
|
-
// Per-user progress for Worksheet Questions
|
|
361
|
-
//
|
|
362
422
|
model WorksheetQuestionProgress {
|
|
363
423
|
id String @id @default(cuid())
|
|
364
424
|
worksheetQuestionId String
|
|
425
|
+
userId String
|
|
426
|
+
userAnswer String?
|
|
427
|
+
completedAt DateTime?
|
|
428
|
+
attempts Int @default(0)
|
|
429
|
+
timeSpentSec Int?
|
|
430
|
+
meta Json?
|
|
431
|
+
createdAt DateTime @default(now())
|
|
432
|
+
updatedAt DateTime @updatedAt
|
|
433
|
+
correct Boolean? @default(false)
|
|
434
|
+
modified Boolean @default(false)
|
|
435
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
365
436
|
worksheetQuestion WorksheetQuestion @relation(fields: [worksheetQuestionId], references: [id], onDelete: Cascade)
|
|
366
437
|
|
|
367
|
-
userId String
|
|
368
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
369
|
-
|
|
370
|
-
modified Boolean @default(false)
|
|
371
|
-
userAnswer String?
|
|
372
|
-
correct Boolean? @default(false)
|
|
373
|
-
completedAt DateTime?
|
|
374
|
-
attempts Int @default(0)
|
|
375
|
-
timeSpentSec Int?
|
|
376
|
-
meta Json?
|
|
377
|
-
|
|
378
|
-
createdAt DateTime @default(now())
|
|
379
|
-
updatedAt DateTime @updatedAt
|
|
380
|
-
|
|
381
438
|
@@unique([worksheetQuestionId, userId])
|
|
382
439
|
@@index([userId])
|
|
383
440
|
}
|
|
384
441
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
442
|
+
model WorksheetPreset {
|
|
443
|
+
id String @id @default(cuid())
|
|
444
|
+
userId String?
|
|
445
|
+
workspaceId String?
|
|
446
|
+
name String
|
|
447
|
+
isSystem Boolean @default(false)
|
|
448
|
+
config Json
|
|
449
|
+
createdAt DateTime @default(now())
|
|
450
|
+
updatedAt DateTime @updatedAt
|
|
451
|
+
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
452
|
+
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
453
|
+
|
|
454
|
+
@@index([userId, workspaceId])
|
|
455
|
+
@@index([isSystem])
|
|
456
|
+
}
|
|
457
|
+
|
|
388
458
|
model WorkspaceMember {
|
|
389
459
|
id String @id @default(cuid())
|
|
390
460
|
workspaceId String
|
|
461
|
+
userId String
|
|
462
|
+
role String @default("member")
|
|
463
|
+
joinedAt DateTime @default(now())
|
|
464
|
+
updatedAt DateTime @updatedAt
|
|
465
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
391
466
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
392
467
|
|
|
393
|
-
userId
|
|
394
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
395
|
-
|
|
396
|
-
role String @default("member") // "owner", "admin", "member"
|
|
397
|
-
|
|
398
|
-
joinedAt DateTime @default(now())
|
|
399
|
-
updatedAt DateTime @updatedAt
|
|
400
|
-
|
|
401
|
-
@@unique([workspaceId, userId]) // One membership per user per workspace
|
|
468
|
+
@@unique([workspaceId, userId])
|
|
402
469
|
@@index([workspaceId])
|
|
403
470
|
@@index([userId])
|
|
404
471
|
}
|
|
405
472
|
|
|
406
|
-
//
|
|
407
|
-
// Workspace Invitations
|
|
408
|
-
//
|
|
409
473
|
model WorkspaceInvitation {
|
|
410
474
|
id String @id @default(cuid())
|
|
411
475
|
workspaceId String
|
|
476
|
+
email String
|
|
477
|
+
role String @default("member")
|
|
478
|
+
token String @unique @default(cuid())
|
|
479
|
+
invitedById String
|
|
480
|
+
acceptedAt DateTime?
|
|
481
|
+
expiresAt DateTime @default(dbgenerated("(now() + '7 days'::interval)"))
|
|
482
|
+
createdAt DateTime @default(now())
|
|
483
|
+
updatedAt DateTime @updatedAt
|
|
484
|
+
invitedBy User @relation("UserInvitations", fields: [invitedById], references: [id], onDelete: Cascade)
|
|
412
485
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
413
486
|
|
|
414
|
-
email
|
|
415
|
-
|
|
416
|
-
|
|
487
|
+
@@unique([workspaceId, email])
|
|
488
|
+
@@index([token])
|
|
489
|
+
@@index([workspaceId])
|
|
490
|
+
}
|
|
417
491
|
|
|
418
|
-
|
|
419
|
-
|
|
492
|
+
model PodcastSegment {
|
|
493
|
+
id String @id @default(cuid())
|
|
494
|
+
artifactId String
|
|
495
|
+
title String
|
|
496
|
+
content String
|
|
497
|
+
startTime Int
|
|
498
|
+
duration Int
|
|
499
|
+
order Int
|
|
500
|
+
objectKey String?
|
|
501
|
+
audioUrl String?
|
|
502
|
+
keyPoints String[]
|
|
503
|
+
meta Json?
|
|
504
|
+
createdAt DateTime @default(now())
|
|
505
|
+
updatedAt DateTime @updatedAt
|
|
506
|
+
generating Boolean @default(false)
|
|
507
|
+
generatingMetadata Json?
|
|
508
|
+
artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
|
|
420
509
|
|
|
421
|
-
|
|
422
|
-
|
|
510
|
+
@@index([artifactId, order])
|
|
511
|
+
@@index([artifactId, startTime])
|
|
512
|
+
}
|
|
423
513
|
|
|
424
|
-
|
|
425
|
-
|
|
514
|
+
model Invoice {
|
|
515
|
+
id String @id @default(cuid())
|
|
516
|
+
userId String
|
|
517
|
+
subscriptionId String?
|
|
518
|
+
stripeInvoiceId String @unique
|
|
519
|
+
amountPaid Int
|
|
520
|
+
status String
|
|
521
|
+
invoicePdfUrl String?
|
|
522
|
+
hostedInvoiceUrl String?
|
|
523
|
+
paidAt DateTime?
|
|
524
|
+
type InvoiceType @default(SUBSCRIPTION)
|
|
525
|
+
createdAt DateTime @default(now())
|
|
526
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
527
|
+
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
|
426
528
|
|
|
427
|
-
@@
|
|
428
|
-
@@index([token])
|
|
429
|
-
@@index([workspaceId])
|
|
529
|
+
@@index([userId])
|
|
430
530
|
}
|
|
431
531
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
532
|
+
model Plan {
|
|
533
|
+
id String @id @default(cuid())
|
|
534
|
+
name String
|
|
535
|
+
description String?
|
|
536
|
+
type String
|
|
537
|
+
price Int
|
|
538
|
+
stripePriceId String
|
|
539
|
+
interval String?
|
|
540
|
+
active Boolean @default(true)
|
|
541
|
+
createdAt DateTime @default(now())
|
|
542
|
+
limit PlanLimit?
|
|
543
|
+
subscriptions Subscription[]
|
|
544
|
+
}
|
|
439
545
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
546
|
+
model PlanLimit {
|
|
547
|
+
id String @id @default(cuid())
|
|
548
|
+
planId String @unique
|
|
549
|
+
maxStorageBytes BigInt @default(0)
|
|
550
|
+
maxWorksheets Int @default(0)
|
|
551
|
+
maxFlashcards Int @default(0)
|
|
552
|
+
maxPodcasts Int @default(0)
|
|
553
|
+
maxStudyGuides Int @default(0)
|
|
554
|
+
createdAt DateTime @default(now())
|
|
555
|
+
updatedAt DateTime @updatedAt
|
|
556
|
+
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
557
|
+
}
|
|
445
558
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
559
|
+
model Subscription {
|
|
560
|
+
id String @id @default(cuid())
|
|
561
|
+
userId String
|
|
562
|
+
planId String
|
|
563
|
+
stripeSubscriptionId String @unique
|
|
564
|
+
stripeCustomerId String?
|
|
565
|
+
status String
|
|
566
|
+
currentPeriodStart DateTime
|
|
567
|
+
currentPeriodEnd DateTime
|
|
568
|
+
cancelAt DateTime?
|
|
569
|
+
canceledAt DateTime?
|
|
570
|
+
createdAt DateTime @default(now())
|
|
571
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
572
|
+
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
|
573
|
+
invoices Invoice[]
|
|
449
574
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
575
|
+
@@index([userId])
|
|
576
|
+
@@index([planId])
|
|
577
|
+
}
|
|
453
578
|
|
|
454
|
-
|
|
455
|
-
|
|
579
|
+
model ResourcePrice {
|
|
580
|
+
id String @id @default(cuid())
|
|
581
|
+
resourceType ArtifactType @unique
|
|
582
|
+
priceCents Int
|
|
583
|
+
updatedAt DateTime @updatedAt
|
|
584
|
+
}
|
|
456
585
|
|
|
457
|
-
|
|
458
|
-
|
|
586
|
+
model UserCredit {
|
|
587
|
+
id String @id @default(cuid())
|
|
588
|
+
userId String
|
|
589
|
+
resourceType ArtifactType
|
|
590
|
+
amount Int @default(1)
|
|
591
|
+
stripeSessionId String @unique
|
|
592
|
+
createdAt DateTime @default(now())
|
|
593
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
594
|
+
|
|
595
|
+
@@index([userId])
|
|
596
|
+
}
|
|
459
597
|
|
|
460
|
-
|
|
461
|
-
|
|
598
|
+
model IdempotencyRecord {
|
|
599
|
+
id String @id @default(cuid())
|
|
600
|
+
userId String
|
|
601
|
+
planId String? // The plan being purchased
|
|
602
|
+
resourceType ArtifactType? // The resource for top-ups
|
|
603
|
+
stripeSessionId String? // The Stripe Checkout Session ID
|
|
604
|
+
status String @default("pending") // pending, completed, failed, expired
|
|
605
|
+
activeLockKey String? @unique // Unique lock: "user_id_plan_id" (Nullified when done)
|
|
606
|
+
|
|
607
|
+
createdAt DateTime @default(now())
|
|
608
|
+
updatedAt DateTime @updatedAt
|
|
609
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
610
|
+
|
|
611
|
+
@@index([userId, status])
|
|
612
|
+
@@index([activeLockKey])
|
|
462
613
|
}
|
|
614
|
+
|
|
615
|
+
model StripeEvent {
|
|
616
|
+
id String @id @default(cuid())
|
|
617
|
+
stripeEventId String @unique // Stripe event ID (e.g. evt_...)
|
|
618
|
+
type String // Event type (e.g. checkout.session.completed)
|
|
619
|
+
status String @default("pending") // pending, processed, failed
|
|
620
|
+
error String? // Error message if processing fails
|
|
621
|
+
processedAt DateTime?
|
|
622
|
+
createdAt DateTime @default(now())
|
|
623
|
+
updatedAt DateTime @updatedAt
|
|
624
|
+
|
|
625
|
+
@@index([stripeEventId])
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/// Append-only system / user activity (tRPC and explicit events). Retention via admin or scheduled job.
|
|
629
|
+
model ActivityLog {
|
|
630
|
+
id String @id @default(cuid())
|
|
631
|
+
createdAt DateTime @default(now())
|
|
632
|
+
actorUserId String?
|
|
633
|
+
actor User? @relation("ActivityLogActor", fields: [actorUserId], references: [id], onDelete: SetNull)
|
|
634
|
+
actorEmailSnapshot String?
|
|
635
|
+
action String
|
|
636
|
+
category ActivityLogCategory
|
|
637
|
+
resourceType String?
|
|
638
|
+
resourceId String?
|
|
639
|
+
workspaceId String?
|
|
640
|
+
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: SetNull)
|
|
641
|
+
trpcPath String?
|
|
642
|
+
httpMethod String?
|
|
643
|
+
status ActivityLogStatus
|
|
644
|
+
errorCode String?
|
|
645
|
+
durationMs Int
|
|
646
|
+
ipAddress String?
|
|
647
|
+
userAgent String?
|
|
648
|
+
metadata Json?
|
|
649
|
+
|
|
650
|
+
@@index([createdAt(sort: Desc)])
|
|
651
|
+
@@index([actorUserId, createdAt(sort: Desc)])
|
|
652
|
+
@@index([workspaceId, createdAt(sort: Desc)])
|
|
653
|
+
@@index([category, createdAt(sort: Desc)])
|
|
654
|
+
}
|
|
655
|
+
|