From d7cbf694696c4f24c6513d4280296b50fc2485b8 Mon Sep 17 00:00:00 2001
From: 7dev7urandom <30197373+7dev7urandom@users.noreply.github.com>
Date: Mon, 25 May 2026 21:52:12 -0400
Subject: [PATCH 1/5] Add grading api routes
---
local.env.dist | 1 +
package-lock.json | 494 ++++++++----------
package.json | 1 +
.../03_grading_result/migration.sql | 23 +
src/lib/prisma/schema.prisma | 49 +-
src/lib/server/aws/lambda.ts | 82 +++
src/lib/server/aws/s3.ts | 20 +
src/lib/server/aws/vars.ts | 4 +
src/lib/server/bullmq/BullMQ.ts | 1 +
src/lib/server/bullmq/BullWorker.ts | 12 +
src/lib/server/bullmq/queues.ts | 13 +-
src/lib/server/bullmq/types.ts | 12 +
src/lib/server/job-executors/grading.ts | 107 ++++
src/lib/server/job-executors/index.ts | 1 +
src/lib/server/models/grading.ts | 76 +++
src/lib/valibot.ts | 10 +
.../project/[id=idNumber]/grading/+server.ts | 110 ++++
.../[gradingResultId=idNumber]/+server.ts | 29 +
.../[id=idNumber]/grading/latest/+server.ts | 30 ++
19 files changed, 771 insertions(+), 304 deletions(-)
create mode 100644 src/lib/prisma/migrations/03_grading_result/migration.sql
create mode 100644 src/lib/server/aws/lambda.ts
create mode 100644 src/lib/server/job-executors/grading.ts
create mode 100644 src/lib/server/models/grading.ts
create mode 100644 src/routes/(api)/project/[id=idNumber]/grading/+server.ts
create mode 100644 src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
create mode 100644 src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
diff --git a/local.env.dist b/local.env.dist
index 6b7efc82..74cd1d31 100644
--- a/local.env.dist
+++ b/local.env.dist
@@ -22,6 +22,7 @@ BUILD_ENGINE_SECRETS_BUCKET=SOME_ORG_PREFIX-SOME_APP_ENV-aps-secrets
# Built artifacts will be stored here
BUILD_ENGINE_ARTIFACTS_BUCKET=SOME_ORG_PREFIX-SOME_APP_ENV-aps-artifacts
BUILD_ENGINE_ARTIFACTS_BUCKET_REGION=us-east-1
+BUILD_ENGINE_GRADING_LAMBDA_FUNCTION_NAME=SOME_GRADING_LAMBDA_FUNCTION
# Projects are moved to here by SAB
BUILD_ENGINE_PROJECTS_BUCKET=SOME_ORG_PREFIX-SOME_APP_ENV-aps-projects
diff --git a/package-lock.json b/package-lock.json
index 6ed98fd4..b6b9ac00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@aws-sdk/client-codebuild": "^3.907.0",
"@aws-sdk/client-ecr": "^3.981.0",
"@aws-sdk/client-iam": "^3.911.0",
+ "@aws-sdk/client-lambda": "^3.907.0",
"@aws-sdk/client-s3": "^3.907.0",
"@aws-sdk/client-sts": "^3.907.0",
"@grpc/grpc-js": "^1.14.3",
@@ -1497,6 +1498,67 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/@aws-sdk/client-lambda": {
+ "version": "3.907.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.907.0.tgz",
+ "integrity": "sha512-Xj5vlaWbYKzvE3r+lgBSZhV6XtGGBEMQEor4iez7FWI9jKbA/3saGXBXVj+mrpMAAVk3IPLzwdN/7drY4Ft7HA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.907.0",
+ "@aws-sdk/credential-provider-node": "3.907.0",
+ "@aws-sdk/middleware-host-header": "3.901.0",
+ "@aws-sdk/middleware-logger": "3.901.0",
+ "@aws-sdk/middleware-recursion-detection": "3.901.0",
+ "@aws-sdk/middleware-user-agent": "3.907.0",
+ "@aws-sdk/region-config-resolver": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-endpoints": "3.901.0",
+ "@aws-sdk/util-user-agent-browser": "3.907.0",
+ "@aws-sdk/util-user-agent-node": "3.907.0",
+ "@smithy/config-resolver": "^4.3.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/eventstream-serde-browser": "^4.2.0",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.0",
+ "@smithy/eventstream-serde-node": "^4.2.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/hash-node": "^4.2.0",
+ "@smithy/invalid-dependency": "^4.2.0",
+ "@smithy/middleware-content-length": "^4.2.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/middleware-retry": "^4.4.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/middleware-stack": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/url-parser": "^4.2.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.0",
+ "@smithy/util-defaults-mode-browser": "^4.2.0",
+ "@smithy/util-defaults-mode-node": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-retry": "^4.2.0",
+ "@smithy/util-stream": "^4.4.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-lambda/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@aws-sdk/client-s3": {
"version": "3.907.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.907.0.tgz",
@@ -1792,18 +1854,16 @@
"license": "0BSD"
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz",
- "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==",
+ "version": "3.972.43",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz",
+ "integrity": "sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
- "@aws-sdk/types": "^3.973.1",
- "@smithy/property-provider": "^4.2.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/shared-ini-file-loader": "^4.4.3",
- "@smithy/types": "^4.12.0",
+ "@aws-sdk/core": "^3.974.13",
+ "@aws-sdk/nested-clients": "^3.997.11",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.3",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -1811,86 +1871,18 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": {
- "version": "3.973.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz",
- "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==",
+ "version": "3.974.13",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.13.tgz",
+ "integrity": "sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/xml-builder": "^3.972.2",
- "@smithy/core": "^3.22.0",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/property-provider": "^4.2.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
- "@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-utf8": "^4.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz",
- "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz",
- "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz",
- "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/types": "^3.973.9",
+ "@aws-sdk/xml-builder": "^3.972.25",
"@aws/lambda-invoke-store": "^0.2.2",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz",
- "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
- "@smithy/core": "^3.22.0",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
+ "@smithy/core": "^3.24.3",
+ "@smithy/signature-v4": "^5.4.2",
+ "@smithy/types": "^4.14.2",
+ "bowser": "^2.11.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -1898,64 +1890,36 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz",
- "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==",
+ "version": "3.997.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz",
+ "integrity": "sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/middleware-host-header": "^3.972.3",
- "@aws-sdk/middleware-logger": "^3.972.3",
- "@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
- "@aws-sdk/region-config-resolver": "^3.972.3",
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
- "@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
- "@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
- "@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/hash-node": "^4.2.8",
- "@smithy/invalid-dependency": "^4.2.8",
- "@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
- "@smithy/middleware-serde": "^4.2.9",
- "@smithy/middleware-stack": "^4.2.8",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
- "@smithy/util-endpoints": "^3.2.8",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-retry": "^4.2.8",
- "@smithy/util-utf8": "^4.2.0",
+ "@aws-sdk/core": "^3.974.13",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.28",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.3",
+ "@smithy/fetch-http-handler": "^5.4.3",
+ "@smithy/node-http-handler": "^4.7.3",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz",
- "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==",
+ "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.28",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz",
+ "integrity": "sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/config-resolver": "^4.4.6",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/types": "^4.12.0",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.3",
+ "@smithy/signature-v4": "^5.4.2",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -1963,78 +1927,27 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": {
- "version": "3.973.1",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz",
- "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-endpoints": "^3.2.8",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz",
- "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "bowser": "^2.11.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz",
- "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==",
+ "version": "3.973.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz",
+ "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "^3.972.5",
- "@aws-sdk/types": "^3.973.1",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/types": "^4.12.0",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
- },
- "peerDependencies": {
- "aws-crt": ">=1.0.0"
- },
- "peerDependenciesMeta": {
- "aws-crt": {
- "optional": true
- }
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz",
- "integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==",
+ "version": "3.972.25",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz",
+ "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.12.0",
- "fast-xml-parser": "5.3.4",
+ "@nodable/entities": "2.1.0",
+ "@smithy/types": "^4.14.2",
+ "fast-xml-parser": "5.7.3",
"tslib": "^2.6.2"
},
"engines": {
@@ -2042,18 +1955,18 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
- "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
+ "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/fast-xml-parser": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
- "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz",
+ "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==",
"funding": [
{
"type": "github",
@@ -2062,7 +1975,10 @@
],
"license": "MIT",
"dependencies": {
- "strnum": "^2.1.0"
+ "@nodable/entities": "^2.1.0",
+ "fast-xml-builder": "^1.1.7",
+ "path-expression-matcher": "^1.5.0",
+ "strnum": "^2.2.3"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -3679,6 +3595,18 @@
"win32"
]
},
+ "node_modules/@nodable/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4964,20 +4892,13 @@
"license": "0BSD"
},
"node_modules/@smithy/core": {
- "version": "3.22.1",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz",
- "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==",
+ "version": "3.24.4",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.4.tgz",
+ "integrity": "sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.2.9",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.11",
- "@smithy/util-utf8": "^4.2.0",
- "@smithy/uuid": "^1.1.0",
+ "@aws-crypto/crc32": "5.2.0",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -4991,15 +4912,13 @@
"license": "0BSD"
},
"node_modules/@smithy/credential-provider-imds": {
- "version": "4.2.8",
- "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz",
- "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==",
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.4.tgz",
+ "integrity": "sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/property-provider": "^4.2.8",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5113,15 +5032,13 @@
"license": "0BSD"
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.3.9",
- "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz",
- "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz",
+ "integrity": "sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/querystring-builder": "^4.2.8",
- "@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5385,15 +5302,13 @@
"license": "0BSD"
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.4.9",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz",
- "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==",
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz",
+ "integrity": "sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.2.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/querystring-builder": "^4.2.8",
- "@smithy/types": "^4.12.0",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5444,26 +5359,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/@smithy/querystring-builder": {
- "version": "4.2.8",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz",
- "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^4.12.0",
- "@smithy/util-uri-escape": "^4.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@smithy/querystring-builder/node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
"node_modules/@smithy/querystring-parser": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz",
@@ -5515,18 +5410,13 @@
"license": "0BSD"
},
"node_modules/@smithy/signature-v4": {
- "version": "5.3.8",
- "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz",
- "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.4.tgz",
+ "integrity": "sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/is-array-buffer": "^4.2.0",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "@smithy/util-hex-encoding": "^4.2.0",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-uri-escape": "^4.2.0",
- "@smithy/util-utf8": "^4.2.0",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5564,9 +5454,9 @@
"license": "0BSD"
},
"node_modules/@smithy/types": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz",
- "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==",
+ "version": "4.14.2",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz",
+ "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -5841,24 +5731,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/@smithy/util-uri-escape": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz",
- "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@smithy/util-uri-escape/node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
"node_modules/@smithy/util-utf8": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz",
@@ -8301,6 +8173,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
+ "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.5.0",
+ "xml-naming": "^0.1.0"
+ }
+ },
"node_modules/fast-xml-parser": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
@@ -10259,6 +10147,21 @@
"node": ">=8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
+ "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -11375,9 +11278,9 @@
}
},
"node_modules/strnum": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
- "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
+ "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
"funding": [
{
"type": "github",
@@ -12238,6 +12141,21 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/xml-naming": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
+ "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 48aaadae..3bd12593 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"@aws-sdk/client-codebuild": "^3.907.0",
"@aws-sdk/client-ecr": "^3.981.0",
"@aws-sdk/client-iam": "^3.911.0",
+ "@aws-sdk/client-lambda": "^3.907.0",
"@aws-sdk/client-s3": "^3.907.0",
"@aws-sdk/client-sts": "^3.907.0",
"@grpc/grpc-js": "^1.14.3",
diff --git a/src/lib/prisma/migrations/03_grading_result/migration.sql b/src/lib/prisma/migrations/03_grading_result/migration.sql
new file mode 100644
index 00000000..db82d829
--- /dev/null
+++ b/src/lib/prisma/migrations/03_grading_result/migration.sql
@@ -0,0 +1,23 @@
+-- CreateTable
+CREATE TABLE "public"."gradingResult" (
+ "id" SERIAL NOT NULL,
+ "project_id" INTEGER NOT NULL,
+ "status" VARCHAR(255),
+ "result" VARCHAR(255),
+ "error" VARCHAR(2083),
+ "publisher_id" VARCHAR(255) NOT NULL,
+ "project_url" VARCHAR(1024) NOT NULL,
+ "lambda_request_id" VARCHAR(255),
+ "report_url_base" VARCHAR(2083),
+ "report_files" VARCHAR(255),
+ "created" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
+ "updated" TIMESTAMP(6),
+
+ CONSTRAINT "gradingResult_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "idx_grading_result_project_id" ON "public"."gradingResult"("project_id");
+
+-- AddForeignKey
+ALTER TABLE "public"."gradingResult" ADD CONSTRAINT "fk_grading_result_project_id" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
diff --git a/src/lib/prisma/schema.prisma b/src/lib/prisma/schema.prisma
index 46eb7309..341cf67f 100644
--- a/src/lib/prisma/schema.prisma
+++ b/src/lib/prisma/schema.prisma
@@ -4,7 +4,7 @@ generator client {
datasource db {
provider = "postgresql"
- url = env("DATABASE_URL")
+ url = env("DATABASE_URL")
}
model build {
@@ -62,21 +62,22 @@ model job {
}
model project {
- id Int @id @default(autoincrement())
- status String? @db.VarChar(255)
- result String? @db.VarChar(255)
- error String? @db.VarChar(2083)
- url String? @db.VarChar(1024)
- user_id String? @db.VarChar(255) // ISSUE #77: remove this?
- group_id String? @db.VarChar(255) // ISSUE #77: remove this?
- app_id String? @db.VarChar(255)
- project_name String? @db.VarChar(255)
- language_code String? @db.VarChar(255)
- publishing_key String? @db.VarChar(1024) // ISSUE #77: remove this?
- created DateTime? @default(now()) @db.Timestamp(6)
- updated DateTime? @updatedAt @db.Timestamp(6)
+ id Int @id @default(autoincrement())
+ status String? @db.VarChar(255)
+ result String? @db.VarChar(255)
+ error String? @db.VarChar(2083)
+ url String? @db.VarChar(1024)
+ user_id String? @db.VarChar(255) // ISSUE #77: remove this?
+ group_id String? @db.VarChar(255) // ISSUE #77: remove this?
+ app_id String? @db.VarChar(255)
+ project_name String? @db.VarChar(255)
+ language_code String? @db.VarChar(255)
+ publishing_key String? @db.VarChar(1024) // ISSUE #77: remove this?
+ created DateTime? @default(now()) @db.Timestamp(6)
+ updated DateTime? @updatedAt @db.Timestamp(6)
client_id Int?
- client client? @relation(fields: [client_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_client_id")
+ gradingResult gradingResult[]
+ client client? @relation(fields: [client_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_client_id")
@@index([client_id], map: "idx_project_client_id")
}
@@ -112,3 +113,21 @@ model appVersion {
created DateTime @default(now()) @db.Timestamp(6)
updated DateTime? @updatedAt @db.Timestamp(6)
}
+
+model gradingResult {
+ id Int @id @default(autoincrement())
+ project_id Int
+ status String? @db.VarChar(255)
+ result String? @db.VarChar(255)
+ error String? @db.VarChar(2083)
+ publisher_id String @db.VarChar(255)
+ project_url String @db.VarChar(1024)
+ lambda_request_id String? @db.VarChar(255)
+ report_url_base String? @db.VarChar(2083)
+ report_files String? @db.VarChar(255)
+ created DateTime? @default(now()) @db.Timestamp(6)
+ updated DateTime? @db.Timestamp(6)
+ project project @relation(fields: [project_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_grading_result_project_id")
+
+ @@index([project_id], map: "idx_grading_result_project_id")
+}
diff --git a/src/lib/server/aws/lambda.ts b/src/lib/server/aws/lambda.ts
new file mode 100644
index 00000000..d2d7cbcf
--- /dev/null
+++ b/src/lib/server/aws/lambda.ts
@@ -0,0 +1,82 @@
+import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
+import { SpanStatusCode, trace } from '@opentelemetry/api';
+import { AWSVars } from './vars';
+
+const tracer = trace.getTracer('Lambda');
+
+export class Lambda {
+ private client;
+
+ public constructor() {
+ this.client = new LambdaClient({ region: AWSVars.artifactsRegion() });
+ }
+
+ public async invokeJson>(
+ functionName: string,
+ payload: TPayload
+ ) {
+ return tracer.startActiveSpan('Lambda - Invoke', async (span) => {
+ span.setAttributes({
+ 'lambda.function-name': functionName,
+ 'lambda.payload': JSON.stringify(payload)
+ });
+ try {
+ const startTime = Date.now();
+ const result = await this.client.send(
+ new InvokeCommand({
+ FunctionName: functionName,
+ InvocationType: 'RequestResponse',
+ Payload: Buffer.from(JSON.stringify(payload))
+ })
+ );
+ span.setAttributes({
+ 'lambda.status-code': result.StatusCode,
+ 'lambda.executed-version': result.ExecutedVersion ?? '',
+ 'lambda.function-error': result.FunctionError ?? '',
+ 'lambda.executionTimeMs': Date.now() - startTime
+ });
+
+ const body = result.Payload ? Buffer.from(result.Payload).toString('utf8') : '';
+ const parsed = body ? this.parsePayload(body) : null;
+ if (result.FunctionError) {
+ throw new Error(`Lambda function error: ${result.FunctionError} ${body}`.trim());
+ }
+ if (!result.StatusCode || result.StatusCode < 200 || result.StatusCode >= 300) {
+ throw new Error(`Lambda invoke failed with status ${result.StatusCode ?? '(missing)'}`);
+ }
+ if (parsed && typeof parsed === 'object') {
+ if ('success' in parsed && parsed.success === false) {
+ throw new Error(`Lambda reported failure: ${JSON.stringify(parsed)}`);
+ }
+ if ('result' in parsed && parsed.result === 'FAILURE') {
+ throw new Error(`Lambda reported failure: ${JSON.stringify(parsed)}`);
+ }
+ if ('status' in parsed && String(parsed.status).toLowerCase() === 'failure') {
+ throw new Error(`Lambda reported failure: ${JSON.stringify(parsed)}`);
+ }
+ }
+ return {
+ requestId: result.$metadata.requestId ?? null,
+ payload: parsed
+ };
+ } catch (e) {
+ span.recordException(e as Error);
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: (e as Error).message
+ });
+ throw e;
+ } finally {
+ span.end();
+ }
+ });
+ }
+
+ private parsePayload(body: string) {
+ try {
+ return JSON.parse(body) as Record;
+ } catch {
+ throw new Error(`Lambda returned invalid JSON: ${body}`);
+ }
+ }
+}
diff --git a/src/lib/server/aws/s3.ts b/src/lib/server/aws/s3.ts
index 7c7136fd..e0e23255 100644
--- a/src/lib/server/aws/s3.ts
+++ b/src/lib/server/aws/s3.ts
@@ -2,6 +2,7 @@ import {
CopyObjectCommand,
DeleteObjectsCommand,
GetObjectCommand,
+ HeadObjectCommand,
ListObjectsV2Command,
NoSuchKey,
PutObjectCommand,
@@ -237,6 +238,25 @@ export class S3 {
);
handleArtifact(artifacts_provider, fileS3Key, fileContents);
}
+
+ public async objectExists(key: string) {
+ const bucket = AWSVars.artifacts();
+ try {
+ await this.s3Client.send(
+ new HeadObjectCommand({
+ Bucket: bucket,
+ Key: key
+ })
+ );
+ return true;
+ } catch (e) {
+ if (e instanceof S3ServiceException && e.$metadata.httpStatusCode === 404) {
+ return false;
+ }
+ throw e;
+ }
+ }
+
public async removeCodeBuildFolder(artifacts_provider: ProviderForPrefix) {
const s3Folder = getBasePrefixUrl(artifacts_provider, 'codebuild-output') + '/';
const s3Bucket = AWSVars.artifacts();
diff --git a/src/lib/server/aws/vars.ts b/src/lib/server/aws/vars.ts
index 45f21835..3102229d 100644
--- a/src/lib/server/aws/vars.ts
+++ b/src/lib/server/aws/vars.ts
@@ -42,6 +42,10 @@ export class AWSVars {
public static scriptsPath() {
return `s3://${AWSVars.projects()}/default`;
}
+
+ public static gradingLambdaFunctionName() {
+ return env.BUILD_ENGINE_GRADING_LAMBDA_FUNCTION_NAME;
+ }
/**
* Get the project name which is the prd or stg plus build_app or publish_app
*
diff --git a/src/lib/server/bullmq/BullMQ.ts b/src/lib/server/bullmq/BullMQ.ts
index ec8fd6c3..4b4249b3 100644
--- a/src/lib/server/bullmq/BullMQ.ts
+++ b/src/lib/server/bullmq/BullMQ.ts
@@ -7,6 +7,7 @@ export const allWorkers = building
new Workers.Builds(),
new Workers.S3(),
new Workers.Releases(),
+ new Workers.Grading(),
new Workers.Polling(),
new Workers.SystemStartup(),
new Workers.SystemRecurring()
diff --git a/src/lib/server/bullmq/BullWorker.ts b/src/lib/server/bullmq/BullWorker.ts
index 799192d8..d385ae81 100644
--- a/src/lib/server/bullmq/BullWorker.ts
+++ b/src/lib/server/bullmq/BullWorker.ts
@@ -150,6 +150,18 @@ export class Releases extends BullWorker {
}
}
+export class Grading extends BullWorker {
+ constructor() {
+ super(BullMQ.QueueName.Grading);
+ }
+ async run(job: Job) {
+ switch (job.data.type) {
+ case BullMQ.JobType.Grading_Generate:
+ return Executor.Grading.generate(job as Job);
+ }
+ }
+}
+
export class Polling extends BullWorker {
constructor() {
super(BullMQ.QueueName.Polling);
diff --git a/src/lib/server/bullmq/queues.ts b/src/lib/server/bullmq/queues.ts
index a1c675a0..f170faf1 100644
--- a/src/lib/server/bullmq/queues.ts
+++ b/src/lib/server/bullmq/queues.ts
@@ -1,7 +1,15 @@
import { Queue } from 'bullmq';
import { BullMQOtel } from 'bullmq-otel';
import { Redis } from 'ioredis';
-import type { BuildJob, PollJob, PublishJob, RecurringJob, S3Job, StartupJob } from './types';
+import type {
+ BuildJob,
+ GradingJob,
+ PollJob,
+ PublishJob,
+ RecurringJob,
+ S3Job,
+ StartupJob
+} from './types';
import { QueueName } from './types';
import { env } from '$env/dynamic/private';
import OTEL from '$lib/otel';
@@ -123,6 +131,8 @@ function createQueues() {
const S3 = new Queue(QueueName.S3, getQueueConfig());
/** Queue for Product Publishing */
const Releases = new Queue(QueueName.Releases, getQueueConfig());
+ /** Queue for Grading report generation */
+ const Grading = new Queue(QueueName.Grading, getQueueConfig());
/** Queue for jobs that poll BuildEngine, such as checking the status of a build */
const Polling = new Queue(QueueName.Polling, getQueueConfig());
/** Queue for jobs that run on startup, such as creating the CodeBuild project */
@@ -133,6 +143,7 @@ function createQueues() {
Builds,
S3,
Releases,
+ Grading,
Polling,
SystemStartup,
SystemRecurring
diff --git a/src/lib/server/bullmq/types.ts b/src/lib/server/bullmq/types.ts
index c6cc206e..8b65734c 100644
--- a/src/lib/server/bullmq/types.ts
+++ b/src/lib/server/bullmq/types.ts
@@ -20,6 +20,7 @@ export enum QueueName {
Builds = 'Builds',
S3 = 'S3',
Releases = 'Releases',
+ Grading = 'Grading',
Polling = 'Polling',
System_Startup = 'System (Startup)',
System_Recurring = 'System (Recurring)'
@@ -35,6 +36,8 @@ export enum JobType {
// Publishing Jobs
Release_Product = 'Release Product',
Release_Cancel = 'Cancel Release',
+ // Grading Jobs
+ Grading_Generate = 'Generate Grading Report',
// S3 Jobs
S3_CopyArtifacts = 'Copy Artifacts to S3',
S3_CopyError = 'Copy Errors to S3',
@@ -85,6 +88,13 @@ export namespace Release {
}
}
+export namespace Grading {
+ export interface Generate {
+ type: JobType.Grading_Generate;
+ gradingResultId: number;
+ }
+}
+
export namespace S3 {
export interface CopyArtifacts {
type: JobType.S3_CopyArtifacts;
@@ -112,6 +122,7 @@ export type Job = JobTypeMap[keyof JobTypeMap];
export type BuildJob = JobTypeMap[JobType.Build_Product | JobType.Build_Cancel];
export type S3Job = JobTypeMap[JobType.S3_CopyArtifacts | JobType.S3_CopyError];
export type PublishJob = JobTypeMap[JobType.Release_Product | JobType.Release_Cancel];
+export type GradingJob = JobTypeMap[JobType.Grading_Generate];
export type PollJob = JobTypeMap[JobType.Poll_Build | JobType.Poll_Release];
export type StartupJob = JobTypeMap[
| JobType.System_CreateCodeBuildProject
@@ -125,6 +136,7 @@ export type JobTypeMap = {
[JobType.Poll_Release]: Polling.Release;
[JobType.Release_Product]: Release.Product;
[JobType.Release_Cancel]: Release.Cancel;
+ [JobType.Grading_Generate]: Grading.Generate;
[JobType.S3_CopyArtifacts]: S3.CopyArtifacts;
[JobType.S3_CopyError]: S3.CopyErrors;
[JobType.System_CreateCodeBuildProject]: System.CreateCodeBuildProject;
diff --git a/src/lib/server/job-executors/grading.ts b/src/lib/server/job-executors/grading.ts
new file mode 100644
index 00000000..83ab3dd0
--- /dev/null
+++ b/src/lib/server/job-executors/grading.ts
@@ -0,0 +1,107 @@
+import type { Prisma } from '@prisma/client';
+import type { Job } from 'bullmq';
+import { Lambda } from '../aws/lambda';
+import { S3 } from '../aws/s3';
+import { AWSVars } from '../aws/vars';
+import type { BullMQ } from '../bullmq';
+import { Grading } from '../models/grading';
+import { prisma } from '../prisma';
+import { trimStrings } from '$lib/valibot';
+
+export async function generate(job: Job): Promise {
+ try {
+ const grading = await findGradingResult(job.data.gradingResultId);
+ if (!grading) {
+ throw new Error(`Grading result ${job.data.gradingResultId} not found`);
+ }
+
+ const functionName = AWSVars.gradingLambdaFunctionName();
+ if (!functionName) {
+ throw new Error('BUILD_ENGINE_GRADING_LAMBDA_FUNCTION_NAME is not configured');
+ }
+
+ await updateGrading(grading.id, { status: Grading.Status.Active });
+ job.updateProgress(10);
+
+ const bucket = AWSVars.artifacts();
+ const region = AWSVars.artifactsRegion();
+ const prefix = Grading.reportPrefix(grading.id);
+ const htmlKey = `${prefix}/report.html`;
+ const jsonKey = `${prefix}/report.json`;
+
+ const lambda = new Lambda();
+ const lambdaResult = await lambda.invokeJson(functionName, {
+ reportId: grading.id,
+ reportPrefix: prefix,
+ project: {
+ id: grading.project.id,
+ appId: grading.project.app_id,
+ name: grading.project.project_name,
+ languageCode: grading.project.language_code,
+ s3Url: grading.project_url
+ },
+ publisherId: grading.publisher_id,
+ secrets: {
+ bucket: AWSVars.secrets()
+ },
+ artifacts: {
+ bucket,
+ region,
+ htmlKey,
+ jsonKey
+ }
+ });
+ job.updateProgress(75);
+
+ const s3 = new S3();
+ const [htmlExists, jsonExists] = await Promise.all([
+ s3.objectExists(htmlKey),
+ s3.objectExists(jsonKey)
+ ]);
+ if (!htmlExists || !jsonExists) {
+ throw new Error(
+ `Grading Lambda did not create expected report objects: ${[
+ htmlExists ? null : htmlKey,
+ jsonExists ? null : jsonKey
+ ]
+ .filter(Boolean)
+ .join(', ')}`
+ );
+ }
+
+ const updated = await updateGrading(grading.id, {
+ status: Grading.Status.Completed,
+ result: Grading.Result.Success,
+ lambda_request_id: lambdaResult.requestId,
+ report_url_base: Grading.reportUrlBase(grading.id, bucket),
+ report_files: Grading.reportFiles.join(',')
+ });
+ job.updateProgress(100);
+ return updated;
+ } catch (e) {
+ job.log(`${e}`);
+ await updateGrading(job.data.gradingResultId, {
+ status: Grading.Status.Completed,
+ result: Grading.Result.Failure,
+ error: String(e)
+ });
+ throw e;
+ }
+}
+
+async function findGradingResult(id: number) {
+ return await prisma.gradingResult.findUnique({
+ where: { id },
+ include: {
+ project: true
+ }
+ });
+}
+
+async function updateGrading(id: number, data: Prisma.gradingResultUpdateInput) {
+ const trimmed = trimStrings(data, 'grading');
+ return await prisma.gradingResult.update({
+ where: { id },
+ data: trimmed
+ });
+}
diff --git a/src/lib/server/job-executors/index.ts b/src/lib/server/job-executors/index.ts
index 24a06146..28331cd0 100644
--- a/src/lib/server/job-executors/index.ts
+++ b/src/lib/server/job-executors/index.ts
@@ -1,4 +1,5 @@
export * as Build from './build';
+export * as Grading from './grading';
export * as Polling from './polling';
export * as Release from './release';
export * as S3 from './s3';
diff --git a/src/lib/server/models/grading.ts b/src/lib/server/models/grading.ts
new file mode 100644
index 00000000..908291cc
--- /dev/null
+++ b/src/lib/server/models/grading.ts
@@ -0,0 +1,76 @@
+import type { Prisma } from '@prisma/client';
+
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace Grading {
+ export enum Status {
+ Initialized = 'initialized',
+ Active = 'active',
+ Completed = 'completed'
+ }
+
+ export enum Result {
+ Success = 'SUCCESS',
+ Failure = 'FAILURE'
+ }
+
+ export type ResponseBody = Omit<
+ Prisma.GradingResultMinAggregateOutputType,
+ 'publisher_id' | 'project_url' | 'lambda_request_id' | 'report_url_base' | 'report_files'
+ > & {
+ reports: {
+ html?: string;
+ json?: string;
+ };
+ _links: Record;
+ };
+
+ export const reportFiles = ['report.html', 'report.json'] as const;
+
+ export function reportPrefix(id: number) {
+ return `reports/${id}`;
+ }
+
+ export function reportUrlBase(id: number, bucket: string) {
+ return `https://${bucket}.s3.amazonaws.com/${reportPrefix(id)}/`;
+ }
+
+ export function reports(
+ row: Pick
+ ) {
+ const base = row.report_url_base;
+ const files = row.report_files?.split(',') ?? [];
+ return {
+ html: base && files.includes('report.html') ? base + 'report.html' : undefined,
+ json: base && files.includes('report.json') ? base + 'report.json' : undefined
+ };
+ }
+
+ export function response(
+ row: Prisma.GradingResultMinAggregateOutputType,
+ origin: string,
+ extraLinks: Record = {}
+ ): ResponseBody {
+ return {
+ id: row.id,
+ project_id: row.project_id,
+ status: row.status,
+ result: row.result,
+ error: row.error,
+ created: row.created,
+ updated: row.updated,
+ reports: reports(row),
+ _links: {
+ self: {
+ href: `${origin}/project/${row.project_id}/grading/${row.id}`
+ },
+ project: {
+ href: `${origin}/project/${row.project_id}`
+ },
+ latest: {
+ href: `${origin}/project/${row.project_id}/grading/latest`
+ },
+ ...extraLinks
+ }
+ };
+ }
+}
diff --git a/src/lib/valibot.ts b/src/lib/valibot.ts
index cbd25fd4..9c2ff6e2 100644
--- a/src/lib/valibot.ts
+++ b/src/lib/valibot.ts
@@ -95,6 +95,16 @@ export const stringLimits = {
targets: 255,
artifact_url_base: 255,
artifact_files: 255
+ },
+ grading: {
+ status: 255,
+ result: 255,
+ error: 2083,
+ publisher_id: 255,
+ project_url: 1024,
+ lambda_request_id: 255,
+ report_url_base: 2083,
+ report_files: 255
}
} as const;
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
new file mode 100644
index 00000000..7d36380c
--- /dev/null
+++ b/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
@@ -0,0 +1,110 @@
+import * as v from 'valibot';
+import type { RequestHandler } from './$types';
+import { BullMQ, getQueues } from '$lib/server/bullmq';
+import { Grading } from '$lib/server/models/grading';
+import { prisma } from '$lib/server/prisma';
+import { ErrorResponse } from '$lib/utils';
+import { stringLimits } from '$lib/valibot';
+
+const gradingSchema = v.strictObject({
+ publisher_id: v.pipe(v.string(), v.maxBytes(stringLimits.grading.publisher_id))
+});
+
+type ProjectForGrading = {
+ id: number;
+ url: string | null;
+};
+
+async function findProject(id: number, clientId: number | null) {
+ return await prisma.project.findFirst({
+ where: {
+ id,
+ ...(clientId ? { client_id: clientId } : {})
+ },
+ select: {
+ id: true,
+ url: true
+ }
+ });
+}
+
+function origin() {
+ return process.env.ORIGIN || 'http://localhost:8443';
+}
+
+// POST /project/[id]/grading
+export const POST: RequestHandler = async ({ request, params, locals }) => {
+ const parsed = v.safeParse(gradingSchema, await request.json());
+ if (!parsed.success) return ErrorResponse(400, JSON.stringify(v.flatten(parsed.issues)));
+
+ const project = (await findProject(
+ Number(params.id),
+ locals.clientId
+ )) as ProjectForGrading | null;
+ if (!project) return ErrorResponse(404, 'Project not found');
+ if (!project.url?.startsWith('s3://')) {
+ return ErrorResponse(400, 'Project does not have an s3:// URL');
+ }
+
+ const grading = await prisma.gradingResult.create({
+ data: {
+ project_id: project.id,
+ status: Grading.Status.Initialized,
+ publisher_id: parsed.output.publisher_id,
+ project_url: project.url
+ }
+ });
+ if (!grading) return ErrorResponse(500, 'Unable to create grading result');
+
+ await getQueues().Grading.add(
+ `Generate Grading Report #${grading.id} for Project ${project.id}`,
+ {
+ type: BullMQ.JobType.Grading_Generate,
+ gradingResultId: grading.id
+ }
+ );
+
+ return new Response(
+ JSON.stringify(
+ Grading.response(grading, origin(), {
+ list: {
+ href: `${origin()}/project/${project.id}/grading`
+ }
+ })
+ ),
+ { status: 202 }
+ );
+};
+
+// GET /project/[id]/grading
+export const GET: RequestHandler = async ({ params, locals, url }) => {
+ const project = await findProject(Number(params.id), locals.clientId);
+ if (!project) return ErrorResponse(404, 'Project not found');
+
+ const rawLimit = Number(url.searchParams.get('limit') ?? 20);
+ const limit = Math.max(1, Math.min(Number.isFinite(rawLimit) ? rawLimit : 20, 100));
+ const rows = await prisma.gradingResult.findMany({
+ where: {
+ project_id: project.id
+ },
+ orderBy: [{ created: 'desc' }, { id: 'desc' }],
+ take: limit
+ });
+
+ return new Response(
+ JSON.stringify({
+ gradingResults: rows.map((row) => Grading.response(row, origin())),
+ _links: {
+ self: {
+ href: `${origin()}/project/${project.id}/grading`
+ },
+ project: {
+ href: `${origin()}/project/${project.id}`
+ },
+ latest: {
+ href: `${origin()}/project/${project.id}/grading/latest`
+ }
+ }
+ })
+ );
+};
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
new file mode 100644
index 00000000..15f03ff8
--- /dev/null
+++ b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
@@ -0,0 +1,29 @@
+import type { RequestHandler } from './$types';
+import { Grading } from '$lib/server/models/grading';
+import { prisma } from '$lib/server/prisma';
+import { ErrorResponse } from '$lib/utils';
+
+function origin() {
+ return process.env.ORIGIN || 'http://localhost:8443';
+}
+
+// GET /project/[id]/grading/[gradingResultId]
+export const GET: RequestHandler = async ({ params, locals }) => {
+ const project = await prisma.project.findFirst({
+ where: {
+ id: Number(params.id),
+ ...(locals.clientId ? { client_id: locals.clientId } : {})
+ },
+ select: { id: true }
+ });
+ if (!project) return ErrorResponse(404, 'Project not found');
+
+ const grading = await prisma.gradingResult.findUnique({
+ where: {
+ id: Number(params.gradingResultId)
+ }
+ });
+ if (!grading) return ErrorResponse(404, 'Grading result not found');
+
+ return new Response(JSON.stringify(Grading.response(grading, origin())));
+};
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
new file mode 100644
index 00000000..a8aa3ad3
--- /dev/null
+++ b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
@@ -0,0 +1,30 @@
+import type { RequestHandler } from './$types';
+import { Grading } from '$lib/server/models/grading';
+import { prisma } from '$lib/server/prisma';
+import { ErrorResponse } from '$lib/utils';
+
+function origin() {
+ return process.env.ORIGIN || 'http://localhost:8443';
+}
+
+// GET /project/[id]/grading/latest
+export const GET: RequestHandler = async ({ params, locals }) => {
+ const project = await prisma.project.findFirst({
+ where: {
+ id: Number(params.id),
+ ...(locals.clientId ? { client_id: locals.clientId } : {})
+ },
+ select: { id: true }
+ });
+ if (!project) return ErrorResponse(404, 'Project not found');
+
+ const grading = await prisma.gradingResult.findFirst({
+ where: {
+ project_id: project.id
+ },
+ orderBy: [{ created: 'desc' }, { id: 'desc' }]
+ });
+ if (!grading) return ErrorResponse(404, 'Grading result not found');
+
+ return new Response(JSON.stringify(Grading.response(grading, origin())));
+};
From 2ed2728c0cf92b822b5a259d79b317f0442608eb Mon Sep 17 00:00:00 2001
From: 7dev7urandom <30197373+7dev7urandom@users.noreply.github.com>
Date: Mon, 25 May 2026 22:13:34 -0400
Subject: [PATCH 2/5] Add grading reports UI
---
src/lib/{server => }/models/grading.ts | 14 +-
src/lib/server/job-executors/grading.ts | 2 +-
.../project/[id=idNumber]/grading/+server.ts | 2 +-
.../[gradingResultId=idNumber]/+server.ts | 2 +-
.../[id=idNumber]/grading/latest/+server.ts | 2 +-
src/routes/(ui)/+page.svelte | 8 +
src/routes/(ui)/grading-admin/+page.server.ts | 48 ++++++
src/routes/(ui)/grading-admin/+page.svelte | 140 ++++++++++++++++++
.../(ui)/grading-admin/create/+page.server.ts | 66 +++++++++
.../(ui)/grading-admin/create/+page.svelte | 61 ++++++++
.../(ui)/grading-admin/view/+page.server.ts | 27 ++++
.../(ui)/grading-admin/view/+page.svelte | 110 ++++++++++++++
.../(ui)/project-admin/view/+page.server.ts | 8 +
.../(ui)/project-admin/view/+page.svelte | 52 +++++++
14 files changed, 533 insertions(+), 9 deletions(-)
rename src/lib/{server => }/models/grading.ts (86%)
create mode 100644 src/routes/(ui)/grading-admin/+page.server.ts
create mode 100644 src/routes/(ui)/grading-admin/+page.svelte
create mode 100644 src/routes/(ui)/grading-admin/create/+page.server.ts
create mode 100644 src/routes/(ui)/grading-admin/create/+page.svelte
create mode 100644 src/routes/(ui)/grading-admin/view/+page.server.ts
create mode 100644 src/routes/(ui)/grading-admin/view/+page.svelte
diff --git a/src/lib/server/models/grading.ts b/src/lib/models/grading.ts
similarity index 86%
rename from src/lib/server/models/grading.ts
rename to src/lib/models/grading.ts
index 908291cc..a7575940 100644
--- a/src/lib/server/models/grading.ts
+++ b/src/lib/models/grading.ts
@@ -13,8 +13,14 @@ export namespace Grading {
Failure = 'FAILURE'
}
+ export type ResultRow = Prisma.gradingResultGetPayload;
+
+ export type ResultWithProject = Prisma.gradingResultGetPayload<{
+ include: { project: true };
+ }>;
+
export type ResponseBody = Omit<
- Prisma.GradingResultMinAggregateOutputType,
+ ResultRow,
'publisher_id' | 'project_url' | 'lambda_request_id' | 'report_url_base' | 'report_files'
> & {
reports: {
@@ -34,9 +40,7 @@ export namespace Grading {
return `https://${bucket}.s3.amazonaws.com/${reportPrefix(id)}/`;
}
- export function reports(
- row: Pick
- ) {
+ export function reports(row: Pick) {
const base = row.report_url_base;
const files = row.report_files?.split(',') ?? [];
return {
@@ -46,7 +50,7 @@ export namespace Grading {
}
export function response(
- row: Prisma.GradingResultMinAggregateOutputType,
+ row: ResultRow,
origin: string,
extraLinks: Record = {}
): ResponseBody {
diff --git a/src/lib/server/job-executors/grading.ts b/src/lib/server/job-executors/grading.ts
index 83ab3dd0..bdaae519 100644
--- a/src/lib/server/job-executors/grading.ts
+++ b/src/lib/server/job-executors/grading.ts
@@ -4,8 +4,8 @@ import { Lambda } from '../aws/lambda';
import { S3 } from '../aws/s3';
import { AWSVars } from '../aws/vars';
import type { BullMQ } from '../bullmq';
-import { Grading } from '../models/grading';
import { prisma } from '../prisma';
+import { Grading } from '$lib/models/grading';
import { trimStrings } from '$lib/valibot';
export async function generate(job: Job): Promise {
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
index 7d36380c..ac9de30a 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
@@ -1,7 +1,7 @@
import * as v from 'valibot';
import type { RequestHandler } from './$types';
+import { Grading } from '$lib/models/grading';
import { BullMQ, getQueues } from '$lib/server/bullmq';
-import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { ErrorResponse } from '$lib/utils';
import { stringLimits } from '$lib/valibot';
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
index 15f03ff8..3c1955a9 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
@@ -1,5 +1,5 @@
import type { RequestHandler } from './$types';
-import { Grading } from '$lib/server/models/grading';
+import { Grading } from '$lib/models/grading';
import { prisma } from '$lib/server/prisma';
import { ErrorResponse } from '$lib/utils';
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
index a8aa3ad3..1f7836b9 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
@@ -1,5 +1,5 @@
import type { RequestHandler } from './$types';
-import { Grading } from '$lib/server/models/grading';
+import { Grading } from '$lib/models/grading';
import { prisma } from '$lib/server/prisma';
import { ErrorResponse } from '$lib/utils';
diff --git a/src/routes/(ui)/+page.svelte b/src/routes/(ui)/+page.svelte
index b730a1c5..e948e485 100644
--- a/src/routes/(ui)/+page.svelte
+++ b/src/routes/(ui)/+page.svelte
@@ -58,6 +58,14 @@
Project Administration »
+
+
Grading
+
+ View project grading reports, inspect report status, open generated HTML and JSON report
+ files, or request a new grading report for a project.
+
+
Grading Administration »
+
Operation Queue
diff --git a/src/routes/(ui)/grading-admin/+page.server.ts b/src/routes/(ui)/grading-admin/+page.server.ts
new file mode 100644
index 00000000..d45b2caa
--- /dev/null
+++ b/src/routes/(ui)/grading-admin/+page.server.ts
@@ -0,0 +1,48 @@
+import { fail, superValidate } from 'sveltekit-superforms';
+import { valibot } from 'sveltekit-superforms/adapters';
+import type { Actions, PageServerLoad } from './$types';
+import { prisma } from '$lib/server/prisma';
+import { tableSchema } from '$lib/valibot';
+
+export const load = (async () => {
+ const gradingResults = await prisma.gradingResult.findMany({
+ take: 20,
+ orderBy: { id: 'desc' },
+ include: { project: true }
+ });
+ return {
+ gradingResults,
+ count: await prisma.gradingResult.count(),
+ form: await superValidate(
+ {
+ page: {
+ page: 0,
+ size: 20
+ }
+ },
+ valibot(tableSchema)
+ )
+ };
+}) satisfies PageServerLoad;
+
+export const actions: Actions = {
+ page: async function ({ request }) {
+ const form = await superValidate(request, valibot(tableSchema));
+ if (!form.valid) return fail(400, { form, ok: false });
+
+ const gradingResults = await prisma.gradingResult.findMany({
+ orderBy: form.data.sort ? { [form.data.sort.field]: form.data.sort.direction } : undefined,
+ skip: form.data.page.page * form.data.page.size,
+ take: form.data.page.size,
+ include: { project: true }
+ });
+
+ return {
+ form,
+ ok: true,
+ query: {
+ data: gradingResults
+ }
+ };
+ }
+};
diff --git a/src/routes/(ui)/grading-admin/+page.svelte b/src/routes/(ui)/grading-admin/+page.svelte
new file mode 100644
index 00000000..3fde7018
--- /dev/null
+++ b/src/routes/(ui)/grading-admin/+page.svelte
@@ -0,0 +1,140 @@
+
+
+
+
+ Home
+ {$title}
+
+
{$title}
+
Create Report
+
+ Showing
+ {$form.page.page * $form.page.size + 1}-{Math.min(
+ ($form.page.page + 1) * $form.page.size,
+ data.count
+ )}
+
+ of
+ {data.count}
+ items
+
+
0
+ },
+ {
+ id: 'project_id',
+ header: 'Project ID',
+ compare: () => 0
+ },
+ {
+ id: 'status',
+ header: 'Status',
+ compare: () => 0
+ },
+ {
+ id: 'result',
+ header: 'Result',
+ compare: () => 0
+ },
+ {
+ id: 'publisher_id',
+ header: 'Publisher',
+ compare: () => 0
+ },
+ {
+ id: 'created',
+ header: 'Created',
+ compare: () => 0
+ },
+ {
+ id: 'menu',
+ header: ''
+ }
+ ]}
+ serverSide={true}
+ startDesc={true}
+ onSort={(field, direction) => form.update((data) => ({ ...data, sort: { field, direction } }))}
+ >
+ {#snippet row(gradingResult, index)}
+ {@const reports = Grading.reports(gradingResult)}
+
+ | {index + 1} |
+ {gradingResult.id} |
+
+
+ {gradingResult.project_id}
+
+ |
+ {gradingResult.status} |
+ {gradingResult.result} |
+ {gradingResult.publisher_id} |
+ {getTimeDateString(gradingResult.created)} |
+
+ {#if reports.html}
+
+
+
+ {/if}
+ {#if reports.json}
+
+
+
+ {/if}
+
+
+
+ |
+
+ {/snippet}
+
+
+
diff --git a/src/routes/(ui)/grading-admin/create/+page.server.ts b/src/routes/(ui)/grading-admin/create/+page.server.ts
new file mode 100644
index 00000000..2562e5bf
--- /dev/null
+++ b/src/routes/(ui)/grading-admin/create/+page.server.ts
@@ -0,0 +1,66 @@
+import { redirect } from '@sveltejs/kit';
+import { fail, superValidate } from 'sveltekit-superforms';
+import { valibot } from 'sveltekit-superforms/adapters';
+import * as v from 'valibot';
+import type { Actions, PageServerLoad } from './$types';
+import { Grading } from '$lib/models/grading';
+import { BullMQ, getQueues } from '$lib/server/bullmq';
+import { prisma } from '$lib/server/prisma';
+import { idSchema, paramNumber, stringLimits } from '$lib/valibot';
+
+const createSchema = v.object({
+ project_id: idSchema,
+ publisher_id: v.pipe(v.string(), v.maxBytes(stringLimits.grading.publisher_id))
+});
+
+export const load = (async ({ url }) => {
+ const projectId = v.safeParse(v.pipe(paramNumber, idSchema), url.searchParams.get('project_id'));
+ return {
+ form: await superValidate(
+ {
+ project_id: projectId.success ? projectId.output : 0,
+ publisher_id: ''
+ },
+ valibot(createSchema)
+ )
+ };
+}) satisfies PageServerLoad;
+
+export const actions: Actions = {
+ default: async function ({ request }) {
+ const form = await superValidate(request, valibot(createSchema));
+ if (!form.valid) return fail(400, { form, ok: false });
+
+ const project = await prisma.project.findUnique({
+ where: { id: form.data.project_id },
+ select: { id: true, url: true }
+ });
+ if (!project) return fail(404, { form, ok: false, message: 'Project not found' });
+ if (!project.url?.startsWith('s3://')) {
+ return fail(400, {
+ form,
+ ok: false,
+ message: 'Project does not have an s3:// URL'
+ });
+ }
+
+ const gradingResult = await prisma.gradingResult.create({
+ data: {
+ project_id: project.id,
+ publisher_id: form.data.publisher_id,
+ project_url: project.url,
+ status: Grading.Status.Initialized
+ }
+ });
+
+ await getQueues().Grading.add(
+ `Generate Grading Report #${gradingResult.id} for Project ${project.id}`,
+ {
+ type: BullMQ.JobType.Grading_Generate,
+ gradingResultId: gradingResult.id
+ }
+ );
+
+ redirect(303, `/grading-admin/view?id=${gradingResult.id}`);
+ }
+};
diff --git a/src/routes/(ui)/grading-admin/create/+page.svelte b/src/routes/(ui)/grading-admin/create/+page.svelte
new file mode 100644
index 00000000..c7dc8006
--- /dev/null
+++ b/src/routes/(ui)/grading-admin/create/+page.svelte
@@ -0,0 +1,61 @@
+
+
+
+ Home
+ Grading Reports
+ {$title}
+
+
{$title}
+
+{#if $message}
+
{$message}
+{/if}
+
+
+
+
diff --git a/src/routes/(ui)/grading-admin/view/+page.server.ts b/src/routes/(ui)/grading-admin/view/+page.server.ts
new file mode 100644
index 00000000..ae81655b
--- /dev/null
+++ b/src/routes/(ui)/grading-admin/view/+page.server.ts
@@ -0,0 +1,27 @@
+import { error } from '@sveltejs/kit';
+import * as v from 'valibot';
+import type { PageServerLoad } from './$types';
+import { prisma } from '$lib/server/prisma';
+import { idSchema, paramNumber } from '$lib/valibot';
+
+export const load = (async ({ url }) => {
+ const id = v.safeParse(v.pipe(paramNumber, idSchema), url.searchParams.get('id'));
+ if (!id.success) {
+ error(400, `missing id param`);
+ }
+
+ const gradingResult = await prisma.gradingResult.findUnique({
+ where: {
+ id: id.output
+ },
+ include: {
+ project: true
+ }
+ });
+
+ if (!gradingResult) error(404);
+
+ return {
+ gradingResult
+ };
+}) satisfies PageServerLoad;
diff --git a/src/routes/(ui)/grading-admin/view/+page.svelte b/src/routes/(ui)/grading-admin/view/+page.svelte
new file mode 100644
index 00000000..a426b523
--- /dev/null
+++ b/src/routes/(ui)/grading-admin/view/+page.svelte
@@ -0,0 +1,110 @@
+
+
+
+ Home
+ Grading Reports
+ {data.gradingResult.id}
+
+
+
{$title}
+
+
+
+
+
+
+ | ID |
+ {data.gradingResult.id} |
+
+
+ | Project ID |
+
+
+ {data.gradingResult.project_id}
+
+ |
+
+
+ | Project Name |
+ {data.gradingResult.project.project_name} |
+
+
+ | Publisher |
+ {data.gradingResult.publisher_id} |
+
+
+ | Status |
+ {data.gradingResult.status} |
+
+
+ | Result |
+ {data.gradingResult.result} |
+
+
+ | Error |
+
+ {#if data.gradingResult.error?.match(/^https?:/)}
+
+ {data.gradingResult.error}
+
+ {:else}
+ {data.gradingResult.error}
+ {/if}
+ |
+
+
+ | Reports |
+
+ {#if reports.html}
+ HTML
+
+ {/if}
+ {#if reports.json}
+ JSON
+ {/if}
+ |
+
+
+ | Project URL |
+ {data.gradingResult.project_url} |
+
+
+ | Lambda Request ID |
+ {data.gradingResult.lambda_request_id} |
+
+
+ | Created |
+ {getTimeDateString(data.gradingResult.created)} |
+
+
+ | Updated |
+ {getTimeDateString(data.gradingResult.updated)} |
+
+
+
diff --git a/src/routes/(ui)/project-admin/view/+page.server.ts b/src/routes/(ui)/project-admin/view/+page.server.ts
index 0e5247ab..85b43e00 100644
--- a/src/routes/(ui)/project-admin/view/+page.server.ts
+++ b/src/routes/(ui)/project-admin/view/+page.server.ts
@@ -13,6 +13,14 @@ export const load = (async ({ url }) => {
const project = await prisma.project.findUnique({
where: {
id: id.output
+ },
+ include: {
+ gradingResult: {
+ take: 5,
+ orderBy: {
+ id: 'desc'
+ }
+ }
}
});
diff --git a/src/routes/(ui)/project-admin/view/+page.svelte b/src/routes/(ui)/project-admin/view/+page.svelte
index d4c2c7c5..cc60e54b 100644
--- a/src/routes/(ui)/project-admin/view/+page.svelte
+++ b/src/routes/(ui)/project-admin/view/+page.svelte
@@ -2,6 +2,8 @@
import type { PageData } from './$types';
import { page } from '$app/state';
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
+ import IconContainer from '$lib/components/IconContainer.svelte';
+ import { Grading } from '$lib/models/grading';
import { title } from '$lib/stores';
import { getTimeDateString } from '$lib/utils/time';
@@ -24,6 +26,10 @@
+
+
Recent Grading Reports
+
+
+
+
+ | ID |
+ Status |
+ Result |
+ Publisher |
+ Created |
+ |
+
+
+
+ {#each data.project.gradingResult as gradingResult}
+ {@const reports = Grading.reports(gradingResult)}
+
+ | {gradingResult.id} |
+ {gradingResult.status} |
+ {gradingResult.result} |
+ {gradingResult.publisher_id} |
+ {getTimeDateString(gradingResult.created)} |
+
+ {#if reports.html}
+
+
+
+ {/if}
+ {#if reports.json}
+
+
+
+ {/if}
+
+
+
+ |
+
+ {:else}
+
+ | No grading reports |
+
+ {/each}
+
+
From 0c8eaf14dc5f98e14a2a7bc436f59d27c6f7050a Mon Sep 17 00:00:00 2001
From: 7dev7urandom <30197373+7dev7urandom@users.noreply.github.com>
Date: Mon, 1 Jun 2026 11:39:41 -0400
Subject: [PATCH 3/5] Remove unnecessary lambda request info
---
src/lib/server/job-executors/grading.ts | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/src/lib/server/job-executors/grading.ts b/src/lib/server/job-executors/grading.ts
index bdaae519..a54c5454 100644
--- a/src/lib/server/job-executors/grading.ts
+++ b/src/lib/server/job-executors/grading.ts
@@ -24,7 +24,6 @@ export async function generate(job: Job
): Promise): Promise): Promise
Date: Wed, 10 Jun 2026 15:28:42 -0400
Subject: [PATCH 4/5] Refactor gradingResult model to use UUID, update related
logic and routes, and rework report urls
---
.../03_grading_result/migration.sql | 10 +---
src/lib/prisma/schema.prisma | 10 +---
src/lib/server/aws/lambda.ts | 17 ------
src/lib/server/bullmq/types.ts | 2 +-
src/lib/server/job-executors/grading.ts | 56 +++++++++++--------
src/lib/{ => server}/models/grading.ts | 44 +++++++--------
src/lib/valibot.ts | 8 +--
.../project/[id=idNumber]/grading/+server.ts | 11 ++--
.../+server.ts | 3 +-
.../[id=idNumber]/grading/latest/+server.ts | 2 +-
src/routes/(ui)/grading-admin/+page.server.ts | 5 +-
src/routes/(ui)/grading-admin/+page.svelte | 14 ++---
.../(ui)/grading-admin/create/+page.server.ts | 9 ++-
.../(ui)/grading-admin/view/+page.server.ts | 14 +++--
.../(ui)/grading-admin/view/+page.svelte | 34 +++--------
.../(ui)/project-admin/view/+page.server.ts | 9 ++-
.../(ui)/project-admin/view/+page.svelte | 14 ++---
17 files changed, 112 insertions(+), 150 deletions(-)
rename src/lib/{ => server}/models/grading.ts (57%)
rename src/routes/(api)/project/[id=idNumber]/grading/{[gradingResultId=idNumber] => [gradingResultId]}/+server.ts (93%)
diff --git a/src/lib/prisma/migrations/03_grading_result/migration.sql b/src/lib/prisma/migrations/03_grading_result/migration.sql
index db82d829..18612f32 100644
--- a/src/lib/prisma/migrations/03_grading_result/migration.sql
+++ b/src/lib/prisma/migrations/03_grading_result/migration.sql
@@ -1,19 +1,15 @@
-- CreateTable
CREATE TABLE "public"."gradingResult" (
- "id" SERIAL NOT NULL,
+ "uuid" UUID NOT NULL,
"project_id" INTEGER NOT NULL,
"status" VARCHAR(255),
- "result" VARCHAR(255),
- "error" VARCHAR(2083),
+ "result" VARCHAR(2000),
"publisher_id" VARCHAR(255) NOT NULL,
- "project_url" VARCHAR(1024) NOT NULL,
"lambda_request_id" VARCHAR(255),
- "report_url_base" VARCHAR(2083),
- "report_files" VARCHAR(255),
"created" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
"updated" TIMESTAMP(6),
- CONSTRAINT "gradingResult_pkey" PRIMARY KEY ("id")
+ CONSTRAINT "gradingResult_pkey" PRIMARY KEY ("uuid")
);
-- CreateIndex
diff --git a/src/lib/prisma/schema.prisma b/src/lib/prisma/schema.prisma
index 341cf67f..a785ba4f 100644
--- a/src/lib/prisma/schema.prisma
+++ b/src/lib/prisma/schema.prisma
@@ -115,18 +115,14 @@ model appVersion {
}
model gradingResult {
- id Int @id @default(autoincrement())
+ uuid String @id @default(uuid()) @db.Uuid
project_id Int
status String? @db.VarChar(255)
- result String? @db.VarChar(255)
- error String? @db.VarChar(2083)
+ result String? @db.VarChar(2000)
publisher_id String @db.VarChar(255)
- project_url String @db.VarChar(1024)
lambda_request_id String? @db.VarChar(255)
- report_url_base String? @db.VarChar(2083)
- report_files String? @db.VarChar(255)
created DateTime? @default(now()) @db.Timestamp(6)
- updated DateTime? @db.Timestamp(6)
+ updated DateTime? @updatedAt @db.Timestamp(6)
project project @relation(fields: [project_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_grading_result_project_id")
@@index([project_id], map: "idx_grading_result_project_id")
diff --git a/src/lib/server/aws/lambda.ts b/src/lib/server/aws/lambda.ts
index d2d7cbcf..fe59eca4 100644
--- a/src/lib/server/aws/lambda.ts
+++ b/src/lib/server/aws/lambda.ts
@@ -38,23 +38,6 @@ export class Lambda {
const body = result.Payload ? Buffer.from(result.Payload).toString('utf8') : '';
const parsed = body ? this.parsePayload(body) : null;
- if (result.FunctionError) {
- throw new Error(`Lambda function error: ${result.FunctionError} ${body}`.trim());
- }
- if (!result.StatusCode || result.StatusCode < 200 || result.StatusCode >= 300) {
- throw new Error(`Lambda invoke failed with status ${result.StatusCode ?? '(missing)'}`);
- }
- if (parsed && typeof parsed === 'object') {
- if ('success' in parsed && parsed.success === false) {
- throw new Error(`Lambda reported failure: ${JSON.stringify(parsed)}`);
- }
- if ('result' in parsed && parsed.result === 'FAILURE') {
- throw new Error(`Lambda reported failure: ${JSON.stringify(parsed)}`);
- }
- if ('status' in parsed && String(parsed.status).toLowerCase() === 'failure') {
- throw new Error(`Lambda reported failure: ${JSON.stringify(parsed)}`);
- }
- }
return {
requestId: result.$metadata.requestId ?? null,
payload: parsed
diff --git a/src/lib/server/bullmq/types.ts b/src/lib/server/bullmq/types.ts
index 8b65734c..5e510027 100644
--- a/src/lib/server/bullmq/types.ts
+++ b/src/lib/server/bullmq/types.ts
@@ -91,7 +91,7 @@ export namespace Release {
export namespace Grading {
export interface Generate {
type: JobType.Grading_Generate;
- gradingResultId: number;
+ gradingResultUUID: string;
}
}
diff --git a/src/lib/server/job-executors/grading.ts b/src/lib/server/job-executors/grading.ts
index a54c5454..5042ce9c 100644
--- a/src/lib/server/job-executors/grading.ts
+++ b/src/lib/server/job-executors/grading.ts
@@ -4,15 +4,15 @@ import { Lambda } from '../aws/lambda';
import { S3 } from '../aws/s3';
import { AWSVars } from '../aws/vars';
import type { BullMQ } from '../bullmq';
+import { Grading } from '../models/grading';
import { prisma } from '../prisma';
-import { Grading } from '$lib/models/grading';
import { trimStrings } from '$lib/valibot';
export async function generate(job: Job): Promise {
try {
- const grading = await findGradingResult(job.data.gradingResultId);
+ const grading = await findGradingResult(job.data.gradingResultUUID);
if (!grading) {
- throw new Error(`Grading result ${job.data.gradingResultId} not found`);
+ throw new Error(`Grading result ${job.data.gradingResultUUID} not found`);
}
const functionName = AWSVars.gradingLambdaFunctionName();
@@ -20,27 +20,39 @@ export async function generate(job: Job): Promise): Promise;
@@ -19,10 +16,7 @@ export namespace Grading {
include: { project: true };
}>;
- export type ResponseBody = Omit<
- ResultRow,
- 'publisher_id' | 'project_url' | 'lambda_request_id' | 'report_url_base' | 'report_files'
- > & {
+ export type ResponseBody = Omit & {
reports: {
html?: string;
json?: string;
@@ -32,40 +26,42 @@ export namespace Grading {
export const reportFiles = ['report.html', 'report.json'] as const;
- export function reportPrefix(id: number) {
- return `reports/${id}`;
+ export function reportPrefix(uuid: string) {
+ return `reports/${uuid}`;
}
- export function reportUrlBase(id: number, bucket: string) {
- return `https://${bucket}.s3.amazonaws.com/${reportPrefix(id)}/`;
+ export function reportUrlBase(uuid: string, bucket: string) {
+ return `https://${bucket}.s3.amazonaws.com/${reportPrefix(uuid)}/`;
}
- export function reports(row: Pick) {
- const base = row.report_url_base;
- const files = row.report_files?.split(',') ?? [];
+ export function reports(row: Pick) {
+ const base = `https://${AWSVars.artifacts()}.s3.amazonaws.com/${reportPrefix(row.uuid)}/`;
return {
- html: base && files.includes('report.html') ? base + 'report.html' : undefined,
- json: base && files.includes('report.json') ? base + 'report.json' : undefined
+ html: base + 'report.html',
+ json: base + 'report.json'
};
}
export function response(
row: ResultRow,
- origin: string,
+ origin: string = process.env.ORIGIN || 'http://localhost:8443',
extraLinks: Record = {}
): ResponseBody {
return {
- id: row.id,
+ uuid: row.uuid,
project_id: row.project_id,
status: row.status,
result: row.result,
- error: row.error,
created: row.created,
updated: row.updated,
- reports: reports(row),
+ publisher_id: row.publisher_id,
+ reports:
+ row.status === Grading.Status.Success
+ ? Grading.reports(row)
+ : { html: undefined, json: undefined },
_links: {
self: {
- href: `${origin}/project/${row.project_id}/grading/${row.id}`
+ href: `${origin}/project/${row.project_id}/grading/${row.uuid}`
},
project: {
href: `${origin}/project/${row.project_id}`
diff --git a/src/lib/valibot.ts b/src/lib/valibot.ts
index 9c2ff6e2..9983cb11 100644
--- a/src/lib/valibot.ts
+++ b/src/lib/valibot.ts
@@ -98,13 +98,9 @@ export const stringLimits = {
},
grading: {
status: 255,
- result: 255,
- error: 2083,
+ result: 2000,
publisher_id: 255,
- project_url: 1024,
- lambda_request_id: 255,
- report_url_base: 2083,
- report_files: 255
+ lambda_request_id: 255
}
} as const;
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
index ac9de30a..4cba4058 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/+server.ts
@@ -1,7 +1,7 @@
import * as v from 'valibot';
import type { RequestHandler } from './$types';
-import { Grading } from '$lib/models/grading';
import { BullMQ, getQueues } from '$lib/server/bullmq';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { ErrorResponse } from '$lib/utils';
import { stringLimits } from '$lib/valibot';
@@ -50,17 +50,16 @@ export const POST: RequestHandler = async ({ request, params, locals }) => {
data: {
project_id: project.id,
status: Grading.Status.Initialized,
- publisher_id: parsed.output.publisher_id,
- project_url: project.url
+ publisher_id: parsed.output.publisher_id
}
});
if (!grading) return ErrorResponse(500, 'Unable to create grading result');
await getQueues().Grading.add(
- `Generate Grading Report #${grading.id} for Project ${project.id}`,
+ `Generate Grading Report #${grading.uuid} for Project ${project.id}`,
{
type: BullMQ.JobType.Grading_Generate,
- gradingResultId: grading.id
+ gradingResultUUID: grading.uuid
}
);
@@ -87,7 +86,7 @@ export const GET: RequestHandler = async ({ params, locals, url }) => {
where: {
project_id: project.id
},
- orderBy: [{ created: 'desc' }, { id: 'desc' }],
+ orderBy: [{ created: 'desc' }],
take: limit
});
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts
similarity index 93%
rename from src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
rename to src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts
index 3c1955a9..fa92ad41 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId=idNumber]/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts
@@ -20,7 +20,8 @@ export const GET: RequestHandler = async ({ params, locals }) => {
const grading = await prisma.gradingResult.findUnique({
where: {
- id: Number(params.gradingResultId)
+ uuid: params.gradingResultId,
+ project_id: project.id
}
});
if (!grading) return ErrorResponse(404, 'Grading result not found');
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
index 1f7836b9..07e49d1b 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
@@ -22,7 +22,7 @@ export const GET: RequestHandler = async ({ params, locals }) => {
where: {
project_id: project.id
},
- orderBy: [{ created: 'desc' }, { id: 'desc' }]
+ orderBy: [{ created: 'desc' }]
});
if (!grading) return ErrorResponse(404, 'Grading result not found');
diff --git a/src/routes/(ui)/grading-admin/+page.server.ts b/src/routes/(ui)/grading-admin/+page.server.ts
index d45b2caa..d815830d 100644
--- a/src/routes/(ui)/grading-admin/+page.server.ts
+++ b/src/routes/(ui)/grading-admin/+page.server.ts
@@ -1,17 +1,18 @@
import { fail, superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import type { Actions, PageServerLoad } from './$types';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { tableSchema } from '$lib/valibot';
export const load = (async () => {
const gradingResults = await prisma.gradingResult.findMany({
take: 20,
- orderBy: { id: 'desc' },
+ orderBy: { created: 'desc' },
include: { project: true }
});
return {
- gradingResults,
+ gradingResults: gradingResults.map((r) => Grading.response(r)),
count: await prisma.gradingResult.count(),
form: await superValidate(
{
diff --git a/src/routes/(ui)/grading-admin/+page.svelte b/src/routes/(ui)/grading-admin/+page.svelte
index 3fde7018..fd6f6343 100644
--- a/src/routes/(ui)/grading-admin/+page.svelte
+++ b/src/routes/(ui)/grading-admin/+page.svelte
@@ -5,7 +5,6 @@
import IconContainer from '$lib/components/IconContainer.svelte';
import Pagination from '$lib/components/Pagination.svelte';
import SortTable from '$lib/components/SortTable.svelte';
- import { Grading } from '$lib/models/grading';
import { title } from '$lib/stores';
import { getTimeDateString } from '$lib/utils/time';
@@ -101,10 +100,9 @@
onSort={(field, direction) => form.update((data) => ({ ...data, sort: { field, direction } }))}
>
{#snippet row(gradingResult, index)}
- {@const reports = Grading.reports(gradingResult)}
| {index + 1} |
- {gradingResult.id} |
+ {gradingResult.uuid} |
{gradingResult.project_id}
@@ -115,17 +113,17 @@
| {gradingResult.publisher_id} |
{getTimeDateString(gradingResult.created)} |
- {#if reports.html}
-
+ {#if gradingResult.reports.html}
+
{/if}
- {#if reports.json}
-
+ {#if gradingResult.reports.json}
+
{/if}
-
+
|
diff --git a/src/routes/(ui)/grading-admin/create/+page.server.ts b/src/routes/(ui)/grading-admin/create/+page.server.ts
index 2562e5bf..19275fd1 100644
--- a/src/routes/(ui)/grading-admin/create/+page.server.ts
+++ b/src/routes/(ui)/grading-admin/create/+page.server.ts
@@ -3,8 +3,8 @@ import { fail, superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import * as v from 'valibot';
import type { Actions, PageServerLoad } from './$types';
-import { Grading } from '$lib/models/grading';
import { BullMQ, getQueues } from '$lib/server/bullmq';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { idSchema, paramNumber, stringLimits } from '$lib/valibot';
@@ -48,19 +48,18 @@ export const actions: Actions = {
data: {
project_id: project.id,
publisher_id: form.data.publisher_id,
- project_url: project.url,
status: Grading.Status.Initialized
}
});
await getQueues().Grading.add(
- `Generate Grading Report #${gradingResult.id} for Project ${project.id}`,
+ `Generate Grading Report #${gradingResult.uuid} for Project ${project.id}`,
{
type: BullMQ.JobType.Grading_Generate,
- gradingResultId: gradingResult.id
+ gradingResultUUID: gradingResult.uuid
}
);
- redirect(303, `/grading-admin/view?id=${gradingResult.id}`);
+ redirect(303, `/grading-admin/view?id=${gradingResult.uuid}`);
}
};
diff --git a/src/routes/(ui)/grading-admin/view/+page.server.ts b/src/routes/(ui)/grading-admin/view/+page.server.ts
index ae81655b..647f9b22 100644
--- a/src/routes/(ui)/grading-admin/view/+page.server.ts
+++ b/src/routes/(ui)/grading-admin/view/+page.server.ts
@@ -1,27 +1,31 @@
import { error } from '@sveltejs/kit';
import * as v from 'valibot';
import type { PageServerLoad } from './$types';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
-import { idSchema, paramNumber } from '$lib/valibot';
export const load = (async ({ url }) => {
- const id = v.safeParse(v.pipe(paramNumber, idSchema), url.searchParams.get('id'));
+ const id = v.safeParse(v.pipe(v.string(), v.uuid()), url.searchParams.get('id'));
if (!id.success) {
error(400, `missing id param`);
}
- const gradingResult = await prisma.gradingResult.findUnique({
+ const gradingResult = await prisma.gradingResult.findFirst({
where: {
- id: id.output
+ uuid: id.output
},
include: {
project: true
+ },
+ orderBy: {
+ created: 'desc'
}
});
if (!gradingResult) error(404);
return {
- gradingResult
+ rawGradingResult: gradingResult,
+ gradingResult: Grading.response(gradingResult)
};
}) satisfies PageServerLoad;
diff --git a/src/routes/(ui)/grading-admin/view/+page.svelte b/src/routes/(ui)/grading-admin/view/+page.svelte
index a426b523..175c1866 100644
--- a/src/routes/(ui)/grading-admin/view/+page.svelte
+++ b/src/routes/(ui)/grading-admin/view/+page.svelte
@@ -2,7 +2,6 @@
import type { PageData } from './$types';
import { page } from '$app/state';
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
- import { Grading } from '$lib/models/grading';
import { title } from '$lib/stores';
import { getTimeDateString } from '$lib/utils/time';
@@ -13,13 +12,12 @@
}
let { data }: Props = $props();
- const reports = $derived(Grading.reports(data.gradingResult));
Home
Grading Reports
- {data.gradingResult.id}
+ {data.gradingResult.uuid}
{$title}
@@ -40,7 +38,7 @@
| ID |
- {data.gradingResult.id} |
+ {data.gradingResult.uuid} |
| Project ID |
@@ -52,7 +50,7 @@
| Project Name |
- {data.gradingResult.project.project_name} |
+ {data.rawGradingResult.project.project_name} |
| Publisher |
@@ -66,37 +64,21 @@
Result |
{data.gradingResult.result} |
-
- | Error |
-
- {#if data.gradingResult.error?.match(/^https?:/)}
-
- {data.gradingResult.error}
-
- {:else}
- {data.gradingResult.error}
- {/if}
- |
-
| Reports |
- {#if reports.html}
- HTML
+ {#if data.gradingResult.reports.html}
+ HTML
{/if}
- {#if reports.json}
- JSON
+ {#if data.gradingResult.reports.json}
+ JSON
{/if}
|
-
- | Project URL |
- {data.gradingResult.project_url} |
-
| Lambda Request ID |
- {data.gradingResult.lambda_request_id} |
+ {data.rawGradingResult.lambda_request_id} |
| Created |
diff --git a/src/routes/(ui)/project-admin/view/+page.server.ts b/src/routes/(ui)/project-admin/view/+page.server.ts
index 85b43e00..048bb336 100644
--- a/src/routes/(ui)/project-admin/view/+page.server.ts
+++ b/src/routes/(ui)/project-admin/view/+page.server.ts
@@ -1,6 +1,7 @@
import { error } from '@sveltejs/kit';
import * as v from 'valibot';
import type { PageServerLoad } from './$types';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { idSchema, paramNumber } from '$lib/valibot';
@@ -18,7 +19,7 @@ export const load = (async ({ url }) => {
gradingResult: {
take: 5,
orderBy: {
- id: 'desc'
+ created: 'desc'
}
}
}
@@ -26,7 +27,11 @@ export const load = (async ({ url }) => {
if (!project) error(404);
+ const projectToReturn = {
+ ...project,
+ gradingResult: project.gradingResult.map((r) => Grading.response(r))
+ };
return {
- project
+ project: projectToReturn
};
}) satisfies PageServerLoad;
diff --git a/src/routes/(ui)/project-admin/view/+page.svelte b/src/routes/(ui)/project-admin/view/+page.svelte
index cc60e54b..2571df21 100644
--- a/src/routes/(ui)/project-admin/view/+page.svelte
+++ b/src/routes/(ui)/project-admin/view/+page.svelte
@@ -3,7 +3,6 @@
import { page } from '$app/state';
import Breadcrumbs from '$lib/components/Breadcrumbs.svelte';
import IconContainer from '$lib/components/IconContainer.svelte';
- import { Grading } from '$lib/models/grading';
import { title } from '$lib/stores';
import { getTimeDateString } from '$lib/utils/time';
@@ -120,25 +119,24 @@
{#each data.project.gradingResult as gradingResult}
- {@const reports = Grading.reports(gradingResult)}
- | {gradingResult.id} |
+ {gradingResult.uuid} |
{gradingResult.status} |
{gradingResult.result} |
{gradingResult.publisher_id} |
{getTimeDateString(gradingResult.created)} |
- {#if reports.html}
-
+ {#if gradingResult.reports.html}
+
{/if}
- {#if reports.json}
-
+ {#if gradingResult.reports.json}
+
{/if}
-
+
|
From b8c1532101624a3827b9c042de2f47b31ed49abd Mon Sep 17 00:00:00 2001
From: 7dev7urandom <30197373+7dev7urandom@users.noreply.github.com>
Date: Wed, 10 Jun 2026 15:42:05 -0400
Subject: [PATCH 5/5] Lint issues
---
.../project/[id=idNumber]/grading/[gradingResultId]/+server.ts | 2 +-
.../(api)/project/[id=idNumber]/grading/latest/+server.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts
index fa92ad41..5df24403 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/[gradingResultId]/+server.ts
@@ -1,5 +1,5 @@
import type { RequestHandler } from './$types';
-import { Grading } from '$lib/models/grading';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { ErrorResponse } from '$lib/utils';
diff --git a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
index 07e49d1b..882ba6ac 100644
--- a/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
+++ b/src/routes/(api)/project/[id=idNumber]/grading/latest/+server.ts
@@ -1,5 +1,5 @@
import type { RequestHandler } from './$types';
-import { Grading } from '$lib/models/grading';
+import { Grading } from '$lib/server/models/grading';
import { prisma } from '$lib/server/prisma';
import { ErrorResponse } from '$lib/utils';