@studious-lms/server 1.1.22 → 1.1.24
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/API_SPECIFICATION.md +146 -10
- package/BASE64_REMOVAL_SUMMARY.md +11 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +11 -68
- package/dist/lib/googleCloudStorage.d.ts +0 -7
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +0 -19
- package/dist/routers/_app.d.ts +114 -40
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +31 -20
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +105 -25
- package/dist/routers/folder.d.ts +27 -0
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +93 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +1 -2
- package/dist/utils/logger.d.ts +0 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +2 -27
- package/package.json +2 -2
- package/src/lib/fileUpload.ts +11 -69
- package/src/lib/googleCloudStorage.ts +0 -19
- package/src/routers/assignment.ts +127 -26
- package/src/routers/folder.ts +109 -0
- package/src/seedDatabase.ts +1 -2
- package/src/utils/logger.ts +2 -29
- package/tests/setup.ts +9 -3
package/API_SPECIFICATION.md
CHANGED
|
@@ -144,16 +144,28 @@ Easy LMS is a comprehensive Learning Management System built with tRPC, Prisma,
|
|
|
144
144
|
**Input**:
|
|
145
145
|
```typescript
|
|
146
146
|
{
|
|
147
|
-
profile
|
|
147
|
+
profile?: {
|
|
148
|
+
displayName?: string;
|
|
149
|
+
bio?: string;
|
|
150
|
+
location?: string;
|
|
151
|
+
website?: string;
|
|
152
|
+
};
|
|
153
|
+
// For custom profile picture (already uploaded to GCS)
|
|
148
154
|
profilePicture?: {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
filePath: string;
|
|
156
|
+
fileName: string;
|
|
157
|
+
fileType: string; // image/jpeg, image/png, etc.
|
|
158
|
+
fileSize: number;
|
|
159
|
+
};
|
|
160
|
+
// OR use DiceBear avatar URL
|
|
161
|
+
dicebearAvatar?: {
|
|
162
|
+
url: string; // DiceBear avatar URL
|
|
153
163
|
};
|
|
154
164
|
}
|
|
155
165
|
```
|
|
156
166
|
|
|
167
|
+
**Note**: Profile pictures use direct upload to GCS. First get a signed upload URL using `user.getProfilePictureUploadUrl`, upload the file directly to GCS, then call this endpoint with the file path.
|
|
168
|
+
|
|
157
169
|
---
|
|
158
170
|
|
|
159
171
|
## 🏫 Class Management
|
|
@@ -1291,14 +1303,28 @@ Array<{
|
|
|
1291
1303
|
|
|
1292
1304
|
## 📊 Data Models
|
|
1293
1305
|
|
|
1294
|
-
### File Object
|
|
1306
|
+
### File Object (Direct Upload)
|
|
1307
|
+
```typescript
|
|
1308
|
+
{
|
|
1309
|
+
name: string;
|
|
1310
|
+
type: string;
|
|
1311
|
+
size: number;
|
|
1312
|
+
// Note: No data field - files are uploaded directly to GCS
|
|
1313
|
+
}
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
### Uploaded File Object (Database)
|
|
1295
1317
|
```typescript
|
|
1296
1318
|
{
|
|
1297
1319
|
id: string;
|
|
1298
1320
|
name: string;
|
|
1321
|
+
url: string; // GCS file path
|
|
1299
1322
|
type: string;
|
|
1300
1323
|
size: number;
|
|
1301
|
-
|
|
1324
|
+
uploadStatus: 'PENDING' | 'UPLOADING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
|
|
1325
|
+
uploadProgress?: number;
|
|
1326
|
+
createdAt: Date;
|
|
1327
|
+
updatedAt: Date;
|
|
1302
1328
|
}
|
|
1303
1329
|
```
|
|
1304
1330
|
|
|
@@ -1437,21 +1463,131 @@ const assignment = await trpc.assignment.create.mutate({
|
|
|
1437
1463
|
|
|
1438
1464
|
## 📝 Notes for Frontend Developers
|
|
1439
1465
|
|
|
1440
|
-
1. **File Uploads**: Files are
|
|
1466
|
+
1. **File Uploads**: Files are uploaded directly to Google Cloud Storage (GCS) using signed URLs. See the "File Upload Flow" section below for details.
|
|
1441
1467
|
2. **Date Handling**: All dates are ISO 8601 strings
|
|
1442
1468
|
3. **Authentication**: Store JWT token and include in all requests
|
|
1443
1469
|
4. **Real-time Updates**: Use Socket.IO for live updates
|
|
1444
1470
|
5. **Error Handling**: Always handle tRPC errors appropriately
|
|
1445
1471
|
6. **Type Safety**: Use the exported TypeScript types for full type safety
|
|
1446
1472
|
|
|
1473
|
+
### 📤 File Upload Flow (Direct Upload to GCS)
|
|
1474
|
+
|
|
1475
|
+
**Important**: Files are NO LONGER sent as base64 encoded strings. Instead, they are uploaded directly to Google Cloud Storage for better performance and scalability.
|
|
1476
|
+
|
|
1477
|
+
#### Step-by-Step Process:
|
|
1478
|
+
|
|
1479
|
+
**1. Get Signed Upload URLs**
|
|
1480
|
+
```typescript
|
|
1481
|
+
// For assignment attachments
|
|
1482
|
+
const uploadResponse = await trpc.assignment.getAssignmentUploadUrls.mutate({
|
|
1483
|
+
assignmentId: "assignment-id",
|
|
1484
|
+
classId: "class-id",
|
|
1485
|
+
files: [
|
|
1486
|
+
{ name: "homework.pdf", type: "application/pdf", size: 102400 },
|
|
1487
|
+
{ name: "notes.docx", type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", size: 51200 }
|
|
1488
|
+
]
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
// For submission attachments
|
|
1492
|
+
const uploadResponse = await trpc.assignment.getSubmissionUploadUrls.mutate({
|
|
1493
|
+
assignmentId: "assignment-id",
|
|
1494
|
+
classId: "class-id",
|
|
1495
|
+
files: [
|
|
1496
|
+
{ name: "solution.pdf", type: "application/pdf", size: 204800 }
|
|
1497
|
+
]
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
// For folder files
|
|
1501
|
+
const uploadResponse = await trpc.folder.getUploadUrls.mutate({
|
|
1502
|
+
folderId: "folder-id",
|
|
1503
|
+
classId: "class-id",
|
|
1504
|
+
files: [
|
|
1505
|
+
{ name: "lecture.pptx", type: "application/vnd.openxmlformats-officedocument.presentationml.presentation", size: 512000 }
|
|
1506
|
+
]
|
|
1507
|
+
});
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
**2. Upload Files Directly to GCS**
|
|
1511
|
+
```typescript
|
|
1512
|
+
for (const uploadFile of uploadResponse.uploadFiles) {
|
|
1513
|
+
const file = files.find(f => f.name === uploadFile.name);
|
|
1514
|
+
|
|
1515
|
+
// Optional: Track upload progress
|
|
1516
|
+
const xhr = new XMLHttpRequest();
|
|
1517
|
+
xhr.upload.addEventListener('progress', async (e) => {
|
|
1518
|
+
if (e.lengthComputable) {
|
|
1519
|
+
const progress = (e.loaded / e.total) * 100;
|
|
1520
|
+
await trpc.assignment.updateUploadProgress.mutate({
|
|
1521
|
+
fileId: uploadFile.id,
|
|
1522
|
+
progress: Math.round(progress)
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
// Upload to GCS using signed URL
|
|
1528
|
+
await fetch(uploadFile.uploadUrl, {
|
|
1529
|
+
method: 'PUT',
|
|
1530
|
+
body: file,
|
|
1531
|
+
headers: {
|
|
1532
|
+
'Content-Type': file.type
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
```
|
|
1537
|
+
|
|
1538
|
+
**3. Confirm Upload Success**
|
|
1539
|
+
```typescript
|
|
1540
|
+
// Confirm each file upload
|
|
1541
|
+
for (const uploadFile of uploadResponse.uploadFiles) {
|
|
1542
|
+
await trpc.assignment.confirmAssignmentUpload.mutate({
|
|
1543
|
+
fileId: uploadFile.id,
|
|
1544
|
+
uploadSuccess: true // or false if upload failed
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
#### Benefits of Direct Upload:
|
|
1550
|
+
- ✅ **33% smaller payload** (no base64 encoding overhead)
|
|
1551
|
+
- ✅ **Faster uploads** (direct to GCS, no server processing)
|
|
1552
|
+
- ✅ **Better scalability** (server doesn't handle file data)
|
|
1553
|
+
- ✅ **Upload progress tracking** (real-time progress updates)
|
|
1554
|
+
- ✅ **Better error handling** (retry failed uploads individually)
|
|
1555
|
+
|
|
1556
|
+
#### Available Direct Upload Endpoints:
|
|
1557
|
+
|
|
1558
|
+
**Assignment Attachments:**
|
|
1559
|
+
- `assignment.getAssignmentUploadUrls` - Get signed URLs for assignment files
|
|
1560
|
+
- `assignment.confirmAssignmentUpload` - Confirm file upload success
|
|
1561
|
+
|
|
1562
|
+
**Submission Attachments:**
|
|
1563
|
+
- `assignment.getSubmissionUploadUrls` - Get signed URLs for submission files
|
|
1564
|
+
- `assignment.confirmSubmissionUpload` - Confirm file upload success
|
|
1565
|
+
|
|
1566
|
+
**Folder Files:**
|
|
1567
|
+
- `folder.getUploadUrls` - Get signed URLs for folder files
|
|
1568
|
+
- `folder.confirmUpload` - Confirm file upload success
|
|
1569
|
+
|
|
1570
|
+
**Progress Tracking:**
|
|
1571
|
+
- `assignment.updateUploadProgress` - Update upload progress for any file
|
|
1572
|
+
|
|
1447
1573
|
---
|
|
1448
1574
|
|
|
1449
1575
|
*Generated on: September 14, 2025*
|
|
1450
|
-
*Version: 1.
|
|
1451
|
-
*Last Updated:
|
|
1576
|
+
*Version: 1.2.0*
|
|
1577
|
+
*Last Updated: October 2025*
|
|
1452
1578
|
|
|
1453
1579
|
## 📋 Changelog
|
|
1454
1580
|
|
|
1581
|
+
### Version 1.2.0 (October 2025)
|
|
1582
|
+
- 🚀 **BREAKING CHANGE**: Migrated from base64 file uploads to direct GCS uploads
|
|
1583
|
+
- ✅ Added comprehensive direct upload documentation with step-by-step flow
|
|
1584
|
+
- ✅ Updated File Object data models to reflect new upload system
|
|
1585
|
+
- ✅ Added upload status tracking (`PENDING`, `UPLOADING`, `COMPLETED`, `FAILED`, `CANCELLED`)
|
|
1586
|
+
- ✅ Added new direct upload endpoints for assignments, submissions, and folders
|
|
1587
|
+
- ✅ Added upload progress tracking endpoint
|
|
1588
|
+
- ✅ Updated user profile picture upload to support both custom uploads and DiceBear avatars
|
|
1589
|
+
- 📝 Documented all benefits of direct upload approach (33% smaller payload, faster uploads, etc.)
|
|
1590
|
+
|
|
1455
1591
|
### Version 1.1.0 (September 2025)
|
|
1456
1592
|
- ✅ Added complete Folder Management endpoints (`folder.*`)
|
|
1457
1593
|
- ✅ Added Section Management endpoints (`section.*`)
|
|
@@ -118,6 +118,17 @@ for (const uploadFile of uploadResponse.uploadFiles) {
|
|
|
118
118
|
4. **Update PDF generation** in lab chat to use direct upload
|
|
119
119
|
5. **Add cleanup job** for orphaned files
|
|
120
120
|
|
|
121
|
+
## 📝 Documentation Updates (October 23, 2025)
|
|
122
|
+
|
|
123
|
+
- ✅ **API_SPECIFICATION.md** updated with direct upload flow
|
|
124
|
+
- Updated `user.updateProfile` endpoint documentation
|
|
125
|
+
- Updated File Object data models
|
|
126
|
+
- Added comprehensive direct upload flow documentation
|
|
127
|
+
- Added step-by-step examples for frontend developers
|
|
128
|
+
- Updated version to 1.2.0 with breaking changes documented
|
|
129
|
+
- ✅ **All base64 references removed** from code (except comments explaining removal)
|
|
130
|
+
- ✅ **Direct upload endpoints documented** with examples
|
|
131
|
+
|
|
121
132
|
## 📝 Files Modified
|
|
122
133
|
|
|
123
134
|
- `prisma/schema.prisma` - Added upload tracking fields
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileUpload.d.ts","sourceRoot":"","sources":["../../src/lib/fileUpload.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAKD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAKzB;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlE;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"fileUpload.d.ts","sourceRoot":"","sources":["../../src/lib/fileUpload.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAKD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAKzB;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlE;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAqF3B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,OAAO,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,cAAc,EAAE,EACvB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAa7B"}
|
package/dist/lib/fileUpload.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import { getSignedUrl
|
|
3
|
+
import { getSignedUrl } from "./googleCloudStorage.js";
|
|
4
4
|
import { prisma } from "./prisma.js";
|
|
5
|
-
import { logger } from "../utils/logger.js";
|
|
6
5
|
// DEPRECATED: These functions are no longer used - files are uploaded directly to GCS
|
|
7
6
|
// Use createDirectUploadFile() and createDirectUploadFiles() instead
|
|
8
7
|
/**
|
|
@@ -121,11 +120,7 @@ export async function createDirectUploadFile(file, userId, directory, assignment
|
|
|
121
120
|
};
|
|
122
121
|
}
|
|
123
122
|
catch (error) {
|
|
124
|
-
|
|
125
|
-
name: error.name,
|
|
126
|
-
message: error.message,
|
|
127
|
-
stack: error.stack,
|
|
128
|
-
} : error });
|
|
123
|
+
console.error('Error creating direct upload file:', error);
|
|
129
124
|
throw new TRPCError({
|
|
130
125
|
code: 'INTERNAL_SERVER_ERROR',
|
|
131
126
|
message: 'Failed to create direct upload file',
|
|
@@ -140,48 +135,15 @@ export async function createDirectUploadFile(file, userId, directory, assignment
|
|
|
140
135
|
*/
|
|
141
136
|
export async function confirmDirectUpload(fileId, uploadSuccess, errorMessage) {
|
|
142
137
|
try {
|
|
143
|
-
// First fetch the file record to get the object path
|
|
144
|
-
const fileRecord = await prisma.file.findUnique({
|
|
145
|
-
where: { id: fileId },
|
|
146
|
-
select: { path: true }
|
|
147
|
-
});
|
|
148
|
-
if (!fileRecord) {
|
|
149
|
-
throw new TRPCError({
|
|
150
|
-
code: 'NOT_FOUND',
|
|
151
|
-
message: 'File record not found',
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
let actualUploadSuccess = uploadSuccess;
|
|
155
|
-
let actualErrorMessage = errorMessage;
|
|
156
|
-
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
157
|
-
if (uploadSuccess) {
|
|
158
|
-
try {
|
|
159
|
-
const exists = await objectExists(process.env.GOOGLE_CLOUD_BUCKET_NAME, fileRecord.path);
|
|
160
|
-
if (!exists) {
|
|
161
|
-
actualUploadSuccess = false;
|
|
162
|
-
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
163
|
-
logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
logger.error(`Error verifying file existence in GCS for ${fileId}:`, { error: error instanceof Error ? {
|
|
168
|
-
name: error.name,
|
|
169
|
-
message: error.message,
|
|
170
|
-
stack: error.stack,
|
|
171
|
-
} : error });
|
|
172
|
-
actualUploadSuccess = false;
|
|
173
|
-
actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
138
|
const updateData = {
|
|
177
|
-
uploadStatus:
|
|
178
|
-
uploadProgress:
|
|
139
|
+
uploadStatus: uploadSuccess ? 'COMPLETED' : 'FAILED',
|
|
140
|
+
uploadProgress: uploadSuccess ? 100 : 0,
|
|
179
141
|
};
|
|
180
|
-
if (!
|
|
181
|
-
updateData.uploadError =
|
|
142
|
+
if (!uploadSuccess && errorMessage) {
|
|
143
|
+
updateData.uploadError = errorMessage;
|
|
182
144
|
updateData.uploadRetryCount = { increment: 1 };
|
|
183
145
|
}
|
|
184
|
-
if (
|
|
146
|
+
if (uploadSuccess) {
|
|
185
147
|
updateData.uploadedAt = new Date();
|
|
186
148
|
}
|
|
187
149
|
await prisma.file.update({
|
|
@@ -190,7 +152,7 @@ export async function confirmDirectUpload(fileId, uploadSuccess, errorMessage) {
|
|
|
190
152
|
});
|
|
191
153
|
}
|
|
192
154
|
catch (error) {
|
|
193
|
-
|
|
155
|
+
console.error('Error confirming direct upload:', error);
|
|
194
156
|
throw new TRPCError({
|
|
195
157
|
code: 'INTERNAL_SERVER_ERROR',
|
|
196
158
|
message: 'Failed to confirm upload',
|
|
@@ -204,31 +166,16 @@ export async function confirmDirectUpload(fileId, uploadSuccess, errorMessage) {
|
|
|
204
166
|
*/
|
|
205
167
|
export async function updateUploadProgress(fileId, progress) {
|
|
206
168
|
try {
|
|
207
|
-
// await prisma.file.update({
|
|
208
|
-
// where: { id: fileId },
|
|
209
|
-
// data: {
|
|
210
|
-
// uploadStatus: 'UPLOADING',
|
|
211
|
-
// uploadProgress: Math.min(100, Math.max(0, progress))
|
|
212
|
-
// }
|
|
213
|
-
// });
|
|
214
|
-
const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });
|
|
215
|
-
if (!current || ['COMPLETED', 'FAILED', 'CANCELLED'].includes(current.uploadStatus))
|
|
216
|
-
return;
|
|
217
|
-
const clamped = Math.min(100, Math.max(0, progress));
|
|
218
169
|
await prisma.file.update({
|
|
219
170
|
where: { id: fileId },
|
|
220
171
|
data: {
|
|
221
172
|
uploadStatus: 'UPLOADING',
|
|
222
|
-
uploadProgress:
|
|
173
|
+
uploadProgress: Math.min(100, Math.max(0, progress))
|
|
223
174
|
}
|
|
224
175
|
});
|
|
225
176
|
}
|
|
226
177
|
catch (error) {
|
|
227
|
-
|
|
228
|
-
name: error.name,
|
|
229
|
-
message: error.message,
|
|
230
|
-
stack: error.stack,
|
|
231
|
-
} : error });
|
|
178
|
+
console.error('Error updating upload progress:', error);
|
|
232
179
|
throw new TRPCError({
|
|
233
180
|
code: 'INTERNAL_SERVER_ERROR',
|
|
234
181
|
message: 'Failed to update upload progress',
|
|
@@ -250,11 +197,7 @@ export async function createDirectUploadFiles(files, userId, directory, assignme
|
|
|
250
197
|
return await Promise.all(uploadPromises);
|
|
251
198
|
}
|
|
252
199
|
catch (error) {
|
|
253
|
-
|
|
254
|
-
name: error.name,
|
|
255
|
-
message: error.message,
|
|
256
|
-
stack: error.stack,
|
|
257
|
-
} : error });
|
|
200
|
+
console.error('Error creating direct upload files:', error);
|
|
258
201
|
throw new TRPCError({
|
|
259
202
|
code: 'INTERNAL_SERVER_ERROR',
|
|
260
203
|
message: 'Failed to create direct upload files',
|
|
@@ -10,11 +10,4 @@ export declare function getSignedUrl(filePath: string, action?: 'read' | 'write'
|
|
|
10
10
|
* @param filePath The path of the file to delete
|
|
11
11
|
*/
|
|
12
12
|
export declare function deleteFile(filePath: string): Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* Checks if an object exists in Google Cloud Storage
|
|
15
|
-
* @param bucketName The name of the bucket (unused, uses default bucket)
|
|
16
|
-
* @param objectPath The path of the object to check
|
|
17
|
-
* @returns Promise<boolean> True if the object exists, false otherwise
|
|
18
|
-
*/
|
|
19
|
-
export declare function objectExists(bucketName: string, objectPath: string): Promise<boolean>;
|
|
20
13
|
//# sourceMappingURL=googleCloudStorage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"googleCloudStorage.d.ts","sourceRoot":"","sources":["../../src/lib/googleCloudStorage.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,MAAM,wCAAwD,CAAC;AAQ5E;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,GAAG,OAAgB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB7H;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhE
|
|
1
|
+
{"version":3,"file":"googleCloudStorage.d.ts","sourceRoot":"","sources":["../../src/lib/googleCloudStorage.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,MAAM,wCAAwD,CAAC;AAQ5E;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,GAAG,OAAgB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB7H;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhE"}
|
|
@@ -57,22 +57,3 @@ export async function deleteFile(filePath) {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Checks if an object exists in Google Cloud Storage
|
|
62
|
-
* @param bucketName The name of the bucket (unused, uses default bucket)
|
|
63
|
-
* @param objectPath The path of the object to check
|
|
64
|
-
* @returns Promise<boolean> True if the object exists, false otherwise
|
|
65
|
-
*/
|
|
66
|
-
export async function objectExists(bucketName, objectPath) {
|
|
67
|
-
try {
|
|
68
|
-
const [exists] = await bucket.file(objectPath).exists();
|
|
69
|
-
return exists;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
console.error('Error checking if object exists in Google Cloud Storage:', error);
|
|
73
|
-
throw new TRPCError({
|
|
74
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
75
|
-
message: 'Failed to check object existence',
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
package/dist/routers/_app.d.ts
CHANGED
|
@@ -1326,18 +1326,6 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1326
1326
|
student: {
|
|
1327
1327
|
id: string;
|
|
1328
1328
|
username: string;
|
|
1329
|
-
profile: {
|
|
1330
|
-
id: string;
|
|
1331
|
-
location: string | null;
|
|
1332
|
-
userId: string;
|
|
1333
|
-
createdAt: Date;
|
|
1334
|
-
displayName: string | null;
|
|
1335
|
-
bio: string | null;
|
|
1336
|
-
website: string | null;
|
|
1337
|
-
profilePicture: string | null;
|
|
1338
|
-
profilePictureThumbnail: string | null;
|
|
1339
|
-
updatedAt: Date;
|
|
1340
|
-
} | null;
|
|
1341
1329
|
};
|
|
1342
1330
|
attachments: {
|
|
1343
1331
|
path: string;
|
|
@@ -1458,16 +1446,8 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
1458
1446
|
id: string;
|
|
1459
1447
|
username: string;
|
|
1460
1448
|
profile: {
|
|
1461
|
-
id: string;
|
|
1462
|
-
location: string | null;
|
|
1463
|
-
userId: string;
|
|
1464
|
-
createdAt: Date;
|
|
1465
1449
|
displayName: string | null;
|
|
1466
|
-
bio: string | null;
|
|
1467
|
-
website: string | null;
|
|
1468
1450
|
profilePicture: string | null;
|
|
1469
|
-
profilePictureThumbnail: string | null;
|
|
1470
|
-
updatedAt: Date;
|
|
1471
1451
|
} | null;
|
|
1472
1452
|
};
|
|
1473
1453
|
attachments: {
|
|
@@ -2439,6 +2419,37 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
2439
2419
|
};
|
|
2440
2420
|
meta: object;
|
|
2441
2421
|
}>;
|
|
2422
|
+
getAnnotationUploadUrls: import("@trpc/server").TRPCMutationProcedure<{
|
|
2423
|
+
input: {
|
|
2424
|
+
[x: string]: unknown;
|
|
2425
|
+
classId: string;
|
|
2426
|
+
files: {
|
|
2427
|
+
type: string;
|
|
2428
|
+
name: string;
|
|
2429
|
+
size: number;
|
|
2430
|
+
}[];
|
|
2431
|
+
submissionId: string;
|
|
2432
|
+
};
|
|
2433
|
+
output: {
|
|
2434
|
+
success: boolean;
|
|
2435
|
+
uploadFiles: import("../lib/fileUpload.js").DirectUploadFile[];
|
|
2436
|
+
};
|
|
2437
|
+
meta: object;
|
|
2438
|
+
}>;
|
|
2439
|
+
confirmAnnotationUpload: import("@trpc/server").TRPCMutationProcedure<{
|
|
2440
|
+
input: {
|
|
2441
|
+
[x: string]: unknown;
|
|
2442
|
+
classId: string;
|
|
2443
|
+
fileId: string;
|
|
2444
|
+
uploadSuccess: boolean;
|
|
2445
|
+
errorMessage?: string | undefined;
|
|
2446
|
+
};
|
|
2447
|
+
output: {
|
|
2448
|
+
success: boolean;
|
|
2449
|
+
message: string;
|
|
2450
|
+
};
|
|
2451
|
+
meta: object;
|
|
2452
|
+
}>;
|
|
2442
2453
|
updateUploadProgress: import("@trpc/server").TRPCMutationProcedure<{
|
|
2443
2454
|
input: {
|
|
2444
2455
|
fileId: string;
|
|
@@ -3586,6 +3597,32 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
3586
3597
|
}[];
|
|
3587
3598
|
meta: object;
|
|
3588
3599
|
}>;
|
|
3600
|
+
getFolderUploadUrls: import("@trpc/server").TRPCMutationProcedure<{
|
|
3601
|
+
input: {
|
|
3602
|
+
[x: string]: unknown;
|
|
3603
|
+
classId: string;
|
|
3604
|
+
files: {
|
|
3605
|
+
type: string;
|
|
3606
|
+
name: string;
|
|
3607
|
+
size: number;
|
|
3608
|
+
}[];
|
|
3609
|
+
folderId: string;
|
|
3610
|
+
};
|
|
3611
|
+
output: import("../lib/fileUpload.js").DirectUploadFile[];
|
|
3612
|
+
meta: object;
|
|
3613
|
+
}>;
|
|
3614
|
+
confirmFolderUpload: import("@trpc/server").TRPCMutationProcedure<{
|
|
3615
|
+
input: {
|
|
3616
|
+
[x: string]: unknown;
|
|
3617
|
+
classId: string;
|
|
3618
|
+
fileId: string;
|
|
3619
|
+
uploadSuccess: boolean;
|
|
3620
|
+
};
|
|
3621
|
+
output: {
|
|
3622
|
+
success: boolean;
|
|
3623
|
+
};
|
|
3624
|
+
meta: object;
|
|
3625
|
+
}>;
|
|
3589
3626
|
}>>;
|
|
3590
3627
|
notification: import("@trpc/server").TRPCBuiltRouter<{
|
|
3591
3628
|
ctx: import("../trpc.js").Context;
|
|
@@ -5582,18 +5619,6 @@ export declare const createCaller: import("@trpc/server").TRPCRouterCaller<{
|
|
|
5582
5619
|
student: {
|
|
5583
5620
|
id: string;
|
|
5584
5621
|
username: string;
|
|
5585
|
-
profile: {
|
|
5586
|
-
id: string;
|
|
5587
|
-
location: string | null;
|
|
5588
|
-
userId: string;
|
|
5589
|
-
createdAt: Date;
|
|
5590
|
-
displayName: string | null;
|
|
5591
|
-
bio: string | null;
|
|
5592
|
-
website: string | null;
|
|
5593
|
-
profilePicture: string | null;
|
|
5594
|
-
profilePictureThumbnail: string | null;
|
|
5595
|
-
updatedAt: Date;
|
|
5596
|
-
} | null;
|
|
5597
5622
|
};
|
|
5598
5623
|
attachments: {
|
|
5599
5624
|
path: string;
|
|
@@ -5714,16 +5739,8 @@ export declare const createCaller: import("@trpc/server").TRPCRouterCaller<{
|
|
|
5714
5739
|
id: string;
|
|
5715
5740
|
username: string;
|
|
5716
5741
|
profile: {
|
|
5717
|
-
id: string;
|
|
5718
|
-
location: string | null;
|
|
5719
|
-
userId: string;
|
|
5720
|
-
createdAt: Date;
|
|
5721
5742
|
displayName: string | null;
|
|
5722
|
-
bio: string | null;
|
|
5723
|
-
website: string | null;
|
|
5724
5743
|
profilePicture: string | null;
|
|
5725
|
-
profilePictureThumbnail: string | null;
|
|
5726
|
-
updatedAt: Date;
|
|
5727
5744
|
} | null;
|
|
5728
5745
|
};
|
|
5729
5746
|
attachments: {
|
|
@@ -6695,6 +6712,37 @@ export declare const createCaller: import("@trpc/server").TRPCRouterCaller<{
|
|
|
6695
6712
|
};
|
|
6696
6713
|
meta: object;
|
|
6697
6714
|
}>;
|
|
6715
|
+
getAnnotationUploadUrls: import("@trpc/server").TRPCMutationProcedure<{
|
|
6716
|
+
input: {
|
|
6717
|
+
[x: string]: unknown;
|
|
6718
|
+
classId: string;
|
|
6719
|
+
files: {
|
|
6720
|
+
type: string;
|
|
6721
|
+
name: string;
|
|
6722
|
+
size: number;
|
|
6723
|
+
}[];
|
|
6724
|
+
submissionId: string;
|
|
6725
|
+
};
|
|
6726
|
+
output: {
|
|
6727
|
+
success: boolean;
|
|
6728
|
+
uploadFiles: import("../lib/fileUpload.js").DirectUploadFile[];
|
|
6729
|
+
};
|
|
6730
|
+
meta: object;
|
|
6731
|
+
}>;
|
|
6732
|
+
confirmAnnotationUpload: import("@trpc/server").TRPCMutationProcedure<{
|
|
6733
|
+
input: {
|
|
6734
|
+
[x: string]: unknown;
|
|
6735
|
+
classId: string;
|
|
6736
|
+
fileId: string;
|
|
6737
|
+
uploadSuccess: boolean;
|
|
6738
|
+
errorMessage?: string | undefined;
|
|
6739
|
+
};
|
|
6740
|
+
output: {
|
|
6741
|
+
success: boolean;
|
|
6742
|
+
message: string;
|
|
6743
|
+
};
|
|
6744
|
+
meta: object;
|
|
6745
|
+
}>;
|
|
6698
6746
|
updateUploadProgress: import("@trpc/server").TRPCMutationProcedure<{
|
|
6699
6747
|
input: {
|
|
6700
6748
|
fileId: string;
|
|
@@ -7842,6 +7890,32 @@ export declare const createCaller: import("@trpc/server").TRPCRouterCaller<{
|
|
|
7842
7890
|
}[];
|
|
7843
7891
|
meta: object;
|
|
7844
7892
|
}>;
|
|
7893
|
+
getFolderUploadUrls: import("@trpc/server").TRPCMutationProcedure<{
|
|
7894
|
+
input: {
|
|
7895
|
+
[x: string]: unknown;
|
|
7896
|
+
classId: string;
|
|
7897
|
+
files: {
|
|
7898
|
+
type: string;
|
|
7899
|
+
name: string;
|
|
7900
|
+
size: number;
|
|
7901
|
+
}[];
|
|
7902
|
+
folderId: string;
|
|
7903
|
+
};
|
|
7904
|
+
output: import("../lib/fileUpload.js").DirectUploadFile[];
|
|
7905
|
+
meta: object;
|
|
7906
|
+
}>;
|
|
7907
|
+
confirmFolderUpload: import("@trpc/server").TRPCMutationProcedure<{
|
|
7908
|
+
input: {
|
|
7909
|
+
[x: string]: unknown;
|
|
7910
|
+
classId: string;
|
|
7911
|
+
fileId: string;
|
|
7912
|
+
uploadSuccess: boolean;
|
|
7913
|
+
};
|
|
7914
|
+
output: {
|
|
7915
|
+
success: boolean;
|
|
7916
|
+
};
|
|
7917
|
+
meta: object;
|
|
7918
|
+
}>;
|
|
7845
7919
|
}>>;
|
|
7846
7920
|
notification: import("@trpc/server").TRPCBuiltRouter<{
|
|
7847
7921
|
ctx: import("../trpc.js").Context;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_app.d.ts","sourceRoot":"","sources":["../../src/routers/_app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAc1E,eAAO,MAAM,SAAS
|
|
1
|
+
{"version":3,"file":"_app.d.ts","sourceRoot":"","sources":["../../src/routers/_app.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAc1E,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiBpB,CAAC;AAGH,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAG1D,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAAiC,CAAC"}
|