From 34ea3e53a9dfc6fb7193d1e686e9503c0f595b30 Mon Sep 17 00:00:00 2001 From: HanCotterell Date: Tue, 9 Dec 2025 18:40:40 -0800 Subject: [PATCH 1/9] adds script to ensure docker db set up prior to docker application --- Dockerfile | 6 ++++-- docker-compose.yml | 33 +++++++++++++++++++++++++++++++++ package.json | 2 +- wait-for-db.sh | 18 ++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yml create mode 100755 wait-for-db.sh diff --git a/Dockerfile b/Dockerfile index 343c6c8..2917fd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,13 @@ COPY . /app RUN yarn run build RUN npm prune --production --omit=dev RUN rm -rf src/ +RUN mkdir -p /app/dist FROM node:20-alpine3.18 ENV NODE_ENV=production COPY --from=0 /app /app WORKDIR /app -RUN apk add --update --no-cache openssl1.1-compat +RUN apk add --update --no-cache openssl1.1-compat postgresql-client COPY ./docker-entrypoint.sh /docker-entrypoint.sh -CMD /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh +CMD ["/docker-entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2a266d3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + db: + image: postgres:15-alpine + container_name: labs_gql_db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: labsdb + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 2s + retries: 10 + + app: + build: . + container_name: labs_gql_app + depends_on: + db: + condition: service_healthy + ports: + - "4000:4000" + env_file: + - .env + entrypoint: ["./wait-for-db.sh", "db", "5432", "node", "dist/main.js"] + stdin_open: true + tty: true + +volumes: + db_data: \ No newline at end of file diff --git a/package.json b/package.json index 6d61374..14ba1d3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "start": "node dist", "clean": "rm -rf dist", - "build": "npm -s run clean && npm -s run swagger && npm -s run prisma && npx tsc --skipLibCheck && cpy '**/*' '!**/*.ts' ../dist/ --cwd=src/ --no-overwrite --parents", + "build": "npm -s run clean && npm -s run swagger && npm -s run prisma && npx tsc --skipLibCheck && cp -r src/ dist/ --no-clobber --parents", "prisma": "prisma format && prisma generate", "dev": "ts-node-dev --no-notify --respawn --transpile-only src", "debug": "ts-node-dev --no-notify --respawn src", diff --git a/wait-for-db.sh b/wait-for-db.sh new file mode 100755 index 0000000..7fac00a --- /dev/null +++ b/wait-for-db.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# wait-for-db.sh +set -e + +host="$1" +port="${2:-5432}" # default port 5432 +shift 2 + +echo "Waiting for database at $host:$port..." + +# Loop until the TCP port is open +while ! nc -z "$host" "$port"; do + echo "Postgres is unavailable - sleeping" + sleep 1 +done + +echo "Postgres is up - executing command" +exec "$@" From c1aa4c1aef97597fc98896da147e19f64454b8d1 Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Wed, 10 Dec 2025 11:59:54 -0800 Subject: [PATCH 2/9] Adds specific node version and updates packages --- .node-version | 1 + package.json | 1 + yarn.lock | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .node-version diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..dd0cebd --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +20.19.6 diff --git a/package.json b/package.json index 14ba1d3..940b41c 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "gpt-tokenizer": "^2.1.1", "graphql": "^15.5.0", "graphql-type-json": "^0.3.2", + "graphql-upload": "^13.0.0", "graphql-ws": "^5.14.2", "handlebars": "^4.7.8", "json-schema": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index a444e82..126aeb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2585,6 +2585,11 @@ fs-capacitor@^2.0.4: resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA== +fs-capacitor@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-6.2.0.tgz#fa79ac6576629163cb84561995602d8999afb7f5" + integrity sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw== + fs-capacitor@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-8.0.0.tgz#a95cbcf58dd50750fe718a03ec051961ef4e61f4" @@ -2778,6 +2783,16 @@ graphql-type-json@^0.3.2: resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115" integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg== +graphql-upload@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-13.0.0.tgz#1a255b64d3cbf3c9f9171fa62a8fb0b9b59bb1d9" + integrity sha512-YKhx8m/uOtKu4Y1UzBFJhbBGJTlk7k4CydlUUiNrtxnwZv0WigbRHP+DVhRNKt7u7DXOtcKZeYJlGtnMXvreXA== + dependencies: + busboy "^0.3.1" + fs-capacitor "^6.2.0" + http-errors "^1.8.1" + object-path "^0.11.8" + graphql-ws@^5.14.2: version "5.16.2" resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.16.2.tgz#7b0306c1bdb0e97a05e800ccd523f46fb212e37c" @@ -2925,7 +2940,7 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-errors@^1.7.3: +http-errors@^1.7.3, http-errors@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== @@ -4013,7 +4028,7 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object-path@^0.11.4: +object-path@^0.11.4, object-path@^0.11.8: version "0.11.8" resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== From ac22e85ceac2722b715943497e84faa7d5a0291b Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Wed, 10 Dec 2025 12:07:14 -0800 Subject: [PATCH 3/9] adds .nvmrc file --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 From 4fff44a65460d5b5a2fb869bfca522fdf3fcb038 Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Wed, 10 Dec 2025 13:50:16 -0800 Subject: [PATCH 4/9] Removes unnecessary wait-for-db script --- wait-for-db.sh | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100755 wait-for-db.sh diff --git a/wait-for-db.sh b/wait-for-db.sh deleted file mode 100755 index 7fac00a..0000000 --- a/wait-for-db.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# wait-for-db.sh -set -e - -host="$1" -port="${2:-5432}" # default port 5432 -shift 2 - -echo "Waiting for database at $host:$port..." - -# Loop until the TCP port is open -while ! nc -z "$host" "$port"; do - echo "Postgres is unavailable - sleeping" - sleep 1 -done - -echo "Postgres is up - executing command" -exec "$@" From 6edd0417272b247f47a65c149348f2d08a302905 Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Wed, 10 Dec 2025 13:51:06 -0800 Subject: [PATCH 5/9] updated docker-compose with elasticsearch --- docker-compose.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2a266d3..63b0988 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: labsdb ports: - - "5432:5432" + - "5433:5432" volumes: - db_data:/var/lib/postgresql/data healthcheck: @@ -15,19 +15,39 @@ services: interval: 2s retries: 10 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9 + container_name: labs_gql_elasticsearch + environment: + - discovery.type=single-node + - xpack.security.enabled=false + ports: + - "9200:9200" + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -q green || grep -q yellow"] + interval: 5s + retries: 20 + app: build: . container_name: labs_gql_app depends_on: db: condition: service_healthy + elasticsearch: + condition: service_healthy ports: - - "4000:4000" + - "5000:5000" + - "5001:5001" env_file: - .env - entrypoint: ["./wait-for-db.sh", "db", "5432", "node", "dist/main.js"] + environment: + - ELASTIC_URL=http://elasticsearch:9200 stdin_open: true tty: true volumes: - db_data: \ No newline at end of file + db_data: + elasticsearch_data: \ No newline at end of file From cf2da6348478e3099b6bf50bdd75349bb98dbb08 Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Wed, 17 Dec 2025 14:31:38 -0800 Subject: [PATCH 6/9] Further testing of updated db --- package.json | 5 +- scripts/generateToken.ts | 41 +++++++++ scripts/seedDummy.ts | 180 +++++++++++++++++++++++++++++++++++++++ scripts/testQueries.ts | 118 +++++++++++++++++++++++++ 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 scripts/generateToken.ts create mode 100644 scripts/seedDummy.ts create mode 100644 scripts/testQueries.ts diff --git a/package.json b/package.json index 940b41c..4dc83db 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "dev": "ts-node-dev --no-notify --respawn --transpile-only src", "debug": "ts-node-dev --no-notify --respawn src", "send-event-recommendations": "ts-node scripts/sendEventRecommendations.ts", + "seed-dummy": "ts-node scripts/seedDummy.ts", + "generate-token": "ts-node scripts/generateToken.ts", + "test-queries": "ts-node scripts/testQueries.ts", "swagger": "rm src/badgr/Api.ts; swagger-typescript-api -p badgr-api-v2.yaml -n Api2.ts -o ./src/badgr; echo 'type json = JSON;' | cat - src/badgr/Api2.ts > src/badgr/Api.ts; rm src/badgr/Api2.ts" }, "dependencies": { @@ -88,4 +91,4 @@ "ts-node-dev": "^1.1.6", "typescript": "^5.2.2" } -} +} \ No newline at end of file diff --git a/scripts/generateToken.ts b/scripts/generateToken.ts new file mode 100644 index 0000000..09a6c0f --- /dev/null +++ b/scripts/generateToken.ts @@ -0,0 +1,41 @@ +import 'reflect-metadata'; +import { PrismaClient } from '@prisma/client'; +import { signTokenAdmin, signTokenManager, signTokenUser } from '../src/utils'; + +const prisma = new PrismaClient(); + +async function main() { + const eventId = process.argv[2] || 'event-test-2025'; + const role = process.argv[3] || 'admin'; // admin, manager, mentor, or student + + const event = await prisma.event.findUniqueOrThrow({ where: { id: eventId } }); + + let token: string; + + if (role === 'admin') { + token = signTokenAdmin(event); + console.log(`\nAdmin token for event "${eventId}":\n${token}\n`); + } else if (role === 'manager') { + token = signTokenManager(event); + console.log(`\nManager token for event "${eventId}":\n${token}\n`); + } else if (role === 'mentor') { + const mentor = await prisma.mentor.findFirst({ where: { eventId } }); + if (!mentor) throw new Error(`No mentors found for event ${eventId}`); + token = signTokenUser(mentor); + console.log(`\nMentor token for ${mentor.email} in event "${eventId}":\n${token}\n`); + } else if (role === 'student') { + const student = await prisma.student.findFirst({ where: { eventId } }); + if (!student) throw new Error(`No students found for event ${eventId}`); + token = signTokenUser(student); + console.log(`\nStudent token for ${student.email} in event "${eventId}":\n${token}\n`); + } else { + throw new Error(`Unknown role: ${role}. Use admin, manager, mentor, or student.`); + } +} + +main() + .catch((err) => { + console.error(err); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/scripts/seedDummy.ts b/scripts/seedDummy.ts new file mode 100644 index 0000000..e848223 --- /dev/null +++ b/scripts/seedDummy.ts @@ -0,0 +1,180 @@ +import { PrismaClient, Track, ProjectStatus } from '@prisma/client'; + +const prisma = new PrismaClient(); + +function daysFromNow(days: number): Date { + const d = new Date(); + d.setDate(d.getDate() + days); + return d; +} + +async function main(): Promise { + const eventId = 'event-test-2025'; + + const event = await prisma.event.upsert({ + where: { id: eventId }, + update: { + name: 'Dummy Event', + title: 'Dependency Update Test Event', + emailSignature: '— CodeDay Labs', + studentApplicationsEndAt: daysFromNow(14), + mentorApplicationsEndAt: daysFromNow(14), + matchingDueAt: daysFromNow(21), + startsAt: daysFromNow(30), + projectWorkStartsAt: daysFromNow(31), + }, + create: { + id: eventId, + name: 'Dummy Event', + title: 'Dependency Update Test Event', + emailSignature: '— CodeDay Labs', + studentApplicationsStartAt: new Date(), + mentorApplicationsStartAt: new Date(), + studentApplicationsEndAt: daysFromNow(14), + mentorApplicationsEndAt: daysFromNow(14), + matchingStartsAt: null, + matchingDueAt: daysFromNow(21), + matchingEndsAt: null, + startsAt: daysFromNow(30), + projectWorkStartsAt: daysFromNow(31), + studentApplicationSchema: {}, + studentApplicationUi: {}, + studentApplicationPostprocess: {}, + mentorApplicationSchema: {}, + mentorApplicationUi: {}, + mentorApplicationPostprocess: {}, + hasBeginner: true, + hasIntermediate: true, + hasAdvanced: true, + certificationStatements: ['Participants agree to CodeDay Labs testing.'], + defaultWeeks: 4, + isActive: true, + matchPreferenceSubmissionOpen: true, + }, + }); + + const mentorA = await prisma.mentor.upsert({ + where: { + email_eventId: { email: 'mentor.alice@example.com', eventId: event.id }, + }, + update: { + givenName: 'Alice', + surname: 'Mentor', + username: 'mentor.alice', + profile: { bio: 'Senior engineer testing dependencies.' }, + }, + create: { + givenName: 'Alice', + surname: 'Mentor', + username: 'mentor.alice', + email: 'mentor.alice@example.com', + profile: { bio: 'Senior engineer testing dependencies.' }, + timezone: 'UTC', + eventId: event.id, + }, + }); + + const mentorB = await prisma.mentor.upsert({ + where: { + email_eventId: { email: 'mentor.bob@example.com', eventId: event.id }, + }, + update: { + givenName: 'Bob', + surname: 'Mentor', + username: 'mentor.bob', + profile: { bio: 'Backend mentor for testing.' }, + }, + create: { + givenName: 'Bob', + surname: 'Mentor', + username: 'mentor.bob', + email: 'mentor.bob@example.com', + profile: { bio: 'Backend mentor for testing.' }, + timezone: 'UTC', + eventId: event.id, + }, + }); + + const studentA = await prisma.student.upsert({ + where: { + email_eventId: { email: 'student.ava@example.com', eventId: event.id }, + }, + update: { + givenName: 'Ava', + surname: 'Student', + username: 'student.ava', + profile: { interests: ['graphql', 'ts'] }, + }, + create: { + givenName: 'Ava', + surname: 'Student', + username: 'student.ava', + email: 'student.ava@example.com', + profile: { interests: ['graphql', 'ts'] }, + track: Track.BEGINNER, + minHours: 10, + eventId: event.id, + }, + }); + + const studentB = await prisma.student.upsert({ + where: { + email_eventId: { email: 'student.ben@example.com', eventId: event.id }, + }, + update: { + givenName: 'Ben', + surname: 'Student', + username: 'student.ben', + profile: { interests: ['elasticsearch', 'node'] }, + }, + create: { + givenName: 'Ben', + surname: 'Student', + username: 'student.ben', + email: 'student.ben@example.com', + profile: { interests: ['elasticsearch', 'node'] }, + track: Track.INTERMEDIATE, + minHours: 12, + eventId: event.id, + }, + }); + + const project = await prisma.project.upsert({ + where: { id: 'project-test-2025' }, + update: { + description: 'Test project for dependency validation.', + deliverables: 'Working prototype and docs.', + status: ProjectStatus.ACCEPTED, + }, + create: { + id: 'project-test-2025', + description: 'Test project for dependency validation.', + deliverables: 'Working prototype and docs.', + track: Track.INTERMEDIATE, + maxStudents: 3, + status: ProjectStatus.ACCEPTED, + eventId: event.id, + mentors: { connect: [{ id: mentorA.id }, { id: mentorB.id }] }, + students: { connect: [{ id: studentA.id }, { id: studentB.id }] }, + }, + }); + + console.log('Seed complete'); + console.table([ + { entity: 'Event', id: event.id }, + { entity: 'Mentor', id: mentorA.id }, + { entity: 'Mentor', id: mentorB.id }, + { entity: 'Student', id: studentA.id }, + { entity: 'Student', id: studentB.id }, + { entity: 'Project', id: project.id }, + ]); +} + +main() + .catch((err) => { + console.error(err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/testQueries.ts b/scripts/testQueries.ts new file mode 100644 index 0000000..ce7dcb9 --- /dev/null +++ b/scripts/testQueries.ts @@ -0,0 +1,118 @@ +import "dotenv/config"; + +const endpoint = process.env.GRAPHQL_ENDPOINT || "http://localhost:5000/graphql"; +const apiKey = process.env.API_KEY; // Bearer token (labs JWT) if you want the authed queries to run + +const publicQueries: Array<{ name: string; query: string; variables?: Record }> = [ + { + name: "tags", + query: /* GraphQL */ ` + query Tags { + tags { + id + type + mentorDisplayName + studentDisplayName + } + } + `, + }, + { + name: "statTotalOutcomes", + query: /* GraphQL */ ` + query StatTotalOutcomes { + statTotalOutcomes { + key + value + } + } + `, + }, +]; + +// These require a Bearer token (admin/manager/etc.) +const authedQueries: Array<{ name: string; query: string; variables?: Record }> = [ + { + name: "mentors (take 5)", + query: /* GraphQL */ ` + query Mentors { + mentors(take: 5) { + id + email + givenName + surname + status + eventId + } + } + `, + }, + { + name: "students (take 5)", + query: /* GraphQL */ ` + query Students { + students(take: 5) { + id + email + givenName + surname + track + status + eventId + } + } + `, + }, + { + name: "projects (take 5)", + query: /* GraphQL */ ` + query Projects { + projects(take: 5) { + id + description + track + status + eventId + } + } + `, + }, +]; + +async function run() { + console.log(`Using endpoint: ${endpoint}`); + + for (const { name, query, variables } of publicQueries) { + await exec(name, query, variables, false); + } + + if (!apiKey) { + console.log("\n(No API_KEY provided; skipping authed queries.)"); + } else { + for (const { name, query, variables } of authedQueries) { + await exec(name, query, variables, true); + } + } +} + +async function exec(name: string, query: string, variables: Record | undefined, useAuth: boolean) { + try { + const res = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(useAuth && apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + }, + body: JSON.stringify({ query, variables }), + }); + + const json = await res.json(); + console.log(`\n=== ${name} ===`); + console.log(JSON.stringify(json, null, 2)); + } catch (err) { + console.error(`\n=== ${name} FAILED ===`); + console.error(err); + } +} + +run(); From 607180f12461af5fcd0bcddd08bb41e3484c953c Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Wed, 17 Dec 2025 16:09:56 -0800 Subject: [PATCH 7/9] testQueries update --- scripts/testQueries.ts | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/scripts/testQueries.ts b/scripts/testQueries.ts index ce7dcb9..a3d39c0 100644 --- a/scripts/testQueries.ts +++ b/scripts/testQueries.ts @@ -4,22 +4,9 @@ const endpoint = process.env.GRAPHQL_ENDPOINT || "http://localhost:5000/graphql" const apiKey = process.env.API_KEY; // Bearer token (labs JWT) if you want the authed queries to run const publicQueries: Array<{ name: string; query: string; variables?: Record }> = [ - { - name: "tags", - query: /* GraphQL */ ` - query Tags { - tags { - id - type - mentorDisplayName - studentDisplayName - } - } - `, - }, { name: "statTotalOutcomes", - query: /* GraphQL */ ` + query: ` query StatTotalOutcomes { statTotalOutcomes { key @@ -33,8 +20,8 @@ const publicQueries: Array<{ name: string; query: string; variables?: Record }> = [ { - name: "mentors (take 5)", - query: /* GraphQL */ ` + name: "mentors", + query: ` query Mentors { mentors(take: 5) { id @@ -48,8 +35,8 @@ const authedQueries: Array<{ name: string; query: string; variables?: Record Date: Tue, 6 Jan 2026 14:46:23 -0800 Subject: [PATCH 8/9] Bug fix with prisma changes. --- scripts/generateToken.ts | 2 +- src/activities/tasks/issueBadge.ts | 2 +- .../tasks/mentorWriteRecommendations.ts | 182 +++++++++--------- .../tasks/sendSlackOnboardingReminder.ts | 52 ++--- src/activities/tasks/slackInviteChannels.ts | 50 ++--- .../tasks/slackSendEmailResponseReminder.ts | 56 +++--- src/email/postmark.ts | 4 +- src/resolvers/Artifact.ts | 98 +++++----- src/resolvers/FileType.ts | 2 +- src/resolvers/Match.ts | 2 +- src/resolvers/Mentor.ts | 8 +- src/resolvers/Project.ts | 4 +- src/resolvers/Review.ts | 4 +- src/resolvers/ScheduledAnnouncement.ts | 6 +- src/resolvers/Standup.ts | 4 +- src/resolvers/Student.ts | 12 +- src/resolvers/Survey.ts | 8 +- src/resolvers/decorators.ts | 4 +- src/search/getProjectMatches.ts | 2 +- src/types/Artifact.ts | 6 +- src/types/Mentor.ts | 2 +- src/types/Note.ts | 4 +- src/types/Partner.ts | 2 +- src/types/Project.ts | 6 +- src/types/Resource.ts | 2 +- src/types/Student.ts | 4 +- src/types/Survey.ts | 2 +- src/types/SurveyResponse.ts | 12 +- src/types/TagTrainingSubmission.ts | 4 +- src/utils/validateEvent.ts | 10 +- 30 files changed, 278 insertions(+), 278 deletions(-) diff --git a/scripts/generateToken.ts b/scripts/generateToken.ts index 09a6c0f..b5d81ad 100644 --- a/scripts/generateToken.ts +++ b/scripts/generateToken.ts @@ -8,7 +8,7 @@ async function main() { const eventId = process.argv[2] || 'event-test-2025'; const role = process.argv[3] || 'admin'; // admin, manager, mentor, or student - const event = await prisma.event.findUniqueOrThrow({ where: { id: eventId } }); + const event = await prisma.event.findUnique({ where: { id: eventId } }); let token: string; diff --git a/src/activities/tasks/issueBadge.ts b/src/activities/tasks/issueBadge.ts index ecaff5b..5e01d4c 100644 --- a/src/activities/tasks/issueBadge.ts +++ b/src/activities/tasks/issueBadge.ts @@ -58,7 +58,7 @@ export default async function issueBadge({ auth }: Context, args: Partial; -const mentors = await prisma.mentor.findMany({ - where: { - eventId: auth.eventId, - status: 'ACCEPTED', - projects: { some: { status: 'MATCHED' } }, - }, - include: { - event: true, - projects: { where: { status: 'MATCHED' }, include: { students: { where: { status: 'ACCEPTED' } } } }, - targetSurveyResponses: { where: { authorStudentId: { not: null } } }, - } -}); - -const recommendations = [] -for (const mentor of mentors) { - DEBUG(`Creating recommendation for ${mentor.givenName} ${mentor.surname}.`); - const previousParticipation = await prisma.mentor.findMany({ + const mentors = await prisma.mentor.findMany({ where: { - eventId: { not: auth.eventId }, + eventId: auth.eventId, status: 'ACCEPTED', projects: { some: { status: 'MATCHED' } }, - OR: [ - { email: mentor.email }, - { givenName: mentor.givenName, surname: mentor.surname }, - ...(mentor.username ? [{ username: mentor.username }] : [{}]), - ], }, include: { event: true, @@ -122,74 +101,95 @@ for (const mentor of mentors) { } }); - const allMentorship = [ - mentor, - ...previousParticipation, - ] - .map(m => ({ - ...m, - startsAtDateString: DateTime.fromJSDate(m.event.startsAt).toLocaleString(DateTime.DATE_MED), - endsAtDateString: DateTime.fromJSDate(m.event.startsAt).plus({ weeks: m.maxWeeks || 5 }).toLocaleString(DateTime.DATE_MED) - })); - - const allSurveyResponsesAbout = allMentorship.flatMap(p => p.targetSurveyResponses); - const surveyResponsesFreeResponse = allSurveyResponsesAbout - .flatMap(sr => Object.entries(sr.response as object)) - .filter(([, v]) => typeof v === 'string' && v.length > 20) - .map(([k, v]) => `- ${k}: ${v.replace(/(\r\n|\n|\r)/gm, " ")}`) - .slice(0, 20) - .join(`\n`); - - const allMentoredStudents = allMentorship - .flatMap(m => m.projects) - .flatMap(p => p.students); - - const mentorInformation = { - 'Recommendation For': `${mentor.givenName} ${mentor.surname}`, - 'Pronouns': (mentor.profile as any)?.pronouns || 'they/them', - 'Count of Projects Mentored': allMentorship.flatMap(m => m.projects).length, - 'Students Mentored': allMentoredStudents.map(s => s.givenName).join(', '), - 'Count of Students Mentored': allMentoredStudents.length, - 'Mentorship Dates': allMentorship - .map(m => `${m.startsAtDateString}-${m.endsAtDateString}`) - .join(', '), - ...(surveyResponsesFreeResponse.length < 10 ? {} : { - 'What Students Had to Say': `\n` + surveyResponsesFreeResponse, - }), - }; - - const prompt = GPT_PROMPT + `\n\n` + Object.entries(mentorInformation) - .map(([k, v]) => `${k}: ${v}`) - .join(`\n`); - - DEBUG(prompt); - const completion = await openAi.chat.completions.create({ - messages: [ - { role: 'system', content: GPT_SYSTEM }, - { role: 'user', content: prompt }, - ], - model: 'gpt-4', - }); - const result = completion.choices[0].message.content; - DEBUG(result); - - recommendations.push({ - mentor: `${mentor.givenName} ${mentor.surname}`, - linkedIn: (mentor.profile as any)?.linkedIn || '', - ...mentorInformation, - prompt, - result, - }); -} + const recommendations = [] + for (const mentor of mentors) { + DEBUG(`Creating recommendation for ${mentor.givenName} ${mentor.surname}.`); + const previousParticipation = await prisma.mentor.findMany({ + where: { + eventId: { not: auth.eventId }, + status: 'ACCEPTED', + projects: { some: { status: 'MATCHED' } }, + OR: [ + { email: mentor.email }, + { givenName: mentor.givenName, surname: mentor.surname }, + ...(mentor.username ? [{ username: mentor.username }] : [{}]), + ], + }, + include: { + event: true, + projects: { where: { status: 'MATCHED' }, include: { students: { where: { status: 'ACCEPTED' } } } }, + targetSurveyResponses: { where: { authorStudentId: { not: null } } }, + } + }); + + const allMentorship = [ + mentor, + ...previousParticipation, + ] + .map(m => ({ + ...m, + startsAtDateString: DateTime.fromJSDate(m.event.startsAt).toLocaleString(DateTime.DATE_MED), + endsAtDateString: DateTime.fromJSDate(m.event.startsAt).plus({ weeks: m.maxWeeks || 5 }).toLocaleString(DateTime.DATE_MED) + })); + + const allSurveyResponsesAbout = allMentorship.flatMap(p => p.targetSurveyResponses); + const surveyResponsesFreeResponse = allSurveyResponsesAbout + .flatMap(sr => Object.entries(sr.response as object)) + .filter(([, v]) => typeof v === 'string' && v.length > 20) + .map(([k, v]) => `- ${k}: ${v.replace(/(\r\n|\n|\r)/gm, " ")}`) + .slice(0, 20) + .join(`\n`); + + const allMentoredStudents = allMentorship + .flatMap(m => m.projects) + .flatMap(p => p.students); + + const mentorInformation = { + 'Recommendation For': `${mentor.givenName} ${mentor.surname}`, + 'Pronouns': (mentor.profile as any)?.pronouns || 'they/them', + 'Count of Projects Mentored': allMentorship.flatMap(m => m.projects).length, + 'Students Mentored': allMentoredStudents.map(s => s.givenName).join(', '), + 'Count of Students Mentored': allMentoredStudents.length, + 'Mentorship Dates': allMentorship + .map(m => `${m.startsAtDateString}-${m.endsAtDateString}`) + .join(', '), + ...(surveyResponsesFreeResponse.length < 10 ? {} : { + 'What Students Had to Say': `\n` + surveyResponsesFreeResponse, + }), + }; + + const prompt = GPT_PROMPT + `\n\n` + Object.entries(mentorInformation) + .map(([k, v]) => `${k}: ${v}`) + .join(`\n`); + + DEBUG(prompt); + const completion = await openAi.chat.completions.create({ + messages: [ + { role: 'system', content: GPT_SYSTEM }, + { role: 'user', content: prompt }, + ], + model: 'gpt-4', + }); + const result = completion.choices[0].message.content; + DEBUG(result); + + recommendations.push({ + mentor: `${mentor.givenName} ${mentor.surname}`, + linkedIn: (mentor.profile as any)?.linkedIn || '', + ...mentorInformation, + prompt, + result, + }); + } -const csv = stringify(recommendations, { header: true }); -const slack = getSlackClientForEvent(event); + const csv = stringify(recommendations, { header: true }); + const slack = getSlackClientForEvent(event); -await slack.files.upload({ - channels: args.channel, - initial_comment: `Mentor recommendations for ${event.name}`, - content: csv, - filename: `recommendations-${event.id}.csv`, - filetype: 'text/csv', -}); + await slack.files.upload({ + channels: args.channel, + initial_comment: `Mentor recommendations for ${event.name}`, + content: csv, + filename: `recommendations-${event.id}.csv`, + filetype: 'text/csv', + }); } \ No newline at end of file diff --git a/src/activities/tasks/sendSlackOnboardingReminder.ts b/src/activities/tasks/sendSlackOnboardingReminder.ts index f051f3a..17e6354 100644 --- a/src/activities/tasks/sendSlackOnboardingReminder.ts +++ b/src/activities/tasks/sendSlackOnboardingReminder.ts @@ -54,33 +54,33 @@ export default async function slackSendOnboardingReminder({ auth }: Context, arg }, }) as PickNonNullable; -const students = await prisma.student.findMany({ - where: { - eventId: auth.eventId!, - slackId: { not: null }, - ...(args.partnerCode - ? { students: { some: { partnerCode: { equals: args.partnerCode, mode: 'insensitive' } } } } - : {} - ) - }, - select: { slackId: true, tagTrainingSubmissions: { select: { id: true } } } -}); -const filteredStudents = students.filter(s => !s.tagTrainingSubmissions || s.tagTrainingSubmissions.length < (args.min! as number)); + const students = await prisma.student.findMany({ + where: { + eventId: auth.eventId!, + slackId: { not: null }, + ...(args.partnerCode + ? { students: { some: { partnerCode: { equals: args.partnerCode, mode: 'insensitive' } } } } + : {} + ) + }, + select: { slackId: true, tagTrainingSubmissions: { select: { id: true } } } + }); + const filteredStudents = students.filter(s => !s.tagTrainingSubmissions || s.tagTrainingSubmissions.length < (args.min! as number)); -const slack = getSlackClientForEvent(event); + const slack = getSlackClientForEvent(event); -DEBUG(`Reminding ${filteredStudents.length} students about 0-${args.min - 1} onboarding assignments in ${args.channel}.`); -if (filteredStudents.length > 0) { - await slack.chat.postMessage({ - blocks: [{ - type: 'section', - text: { - type: 'mrkdwn', - text: `[${event.name}]\n${args.intro}\n` + filteredStudents.map(s => `- <@${s.slackId}>`).join(`\n`), - } - }], - channel: args.channel, - }); -} + DEBUG(`Reminding ${filteredStudents.length} students about 0-${args.min - 1} onboarding assignments in ${args.channel}.`); + if (filteredStudents.length > 0) { + await slack.chat.postMessage({ + blocks: [{ + type: 'section', + text: { + type: 'mrkdwn', + text: `[${event.name}]\n${args.intro}\n` + filteredStudents.map(s => `- <@${s.slackId}>`).join(`\n`), + } + }], + channel: args.channel, + }); + } } \ No newline at end of file diff --git a/src/activities/tasks/slackInviteChannels.ts b/src/activities/tasks/slackInviteChannels.ts index db074f6..7f7d054 100644 --- a/src/activities/tasks/slackInviteChannels.ts +++ b/src/activities/tasks/slackInviteChannels.ts @@ -40,32 +40,32 @@ export default async function slackInviteChannels({ auth }: Context, args: Parti }, }) as PickNonNullable; -const projects = await prisma.project.findMany({ - where: { - slackChannelId: { not: null }, - eventId: auth.eventId!, - ...(args.partnerCode - ? { students: { some: { partnerCode: { equals: args.partnerCode, mode: 'insensitive' } } } } - : {} - ) - }, - select: { slackChannelId: true }, -}); + const projects = await prisma.project.findMany({ + where: { + slackChannelId: { not: null }, + eventId: auth.eventId!, + ...(args.partnerCode + ? { students: { some: { partnerCode: { equals: args.partnerCode, mode: 'insensitive' } } } } + : {} + ) + }, + select: { slackChannelId: true }, + }); -const slack = getSlackClientForEvent(event); + const slack = getSlackClientForEvent(event); -for (const project of projects) { - DEBUG(`Inviting ${args.user} to ${project.slackChannelId}`); - try { - await slack.conversations.invite({ channel: project.slackChannelId!, users: args.user }); - } catch (ex) { } -} + for (const project of projects) { + DEBUG(`Inviting ${args.user} to ${project.slackChannelId}`); + try { + await slack.conversations.invite({ channel: project.slackChannelId!, users: args.user }); + } catch (ex) { } + } -if (event.slackMentorChannelId && !args.partnerCode) { - DEBUG(`Inviting ${args.user} to mentor channel (${event.slackMentorChannelId})`); - await slack.conversations.invite({ - channel: event.slackMentorChannelId!, - users: args.user, - }); -} + if (event.slackMentorChannelId && !args.partnerCode) { + DEBUG(`Inviting ${args.user} to mentor channel (${event.slackMentorChannelId})`); + await slack.conversations.invite({ + channel: event.slackMentorChannelId!, + users: args.user, + }); + } } \ No newline at end of file diff --git a/src/activities/tasks/slackSendEmailResponseReminder.ts b/src/activities/tasks/slackSendEmailResponseReminder.ts index 1253f04..ff297ea 100644 --- a/src/activities/tasks/slackSendEmailResponseReminder.ts +++ b/src/activities/tasks/slackSendEmailResponseReminder.ts @@ -51,34 +51,34 @@ export default async function slackSendEmailResponseReminder({ auth }: Context, }, }) as PickNonNullable; -const students = await prisma.student.findMany({ - where: { - eventId: auth.eventId!, - projectEmails: args.emailId - ? { none: { emailSent: { emailId: args.emailId } } } - : { none: {} }, - slackId: { not: null }, - ...(args.partnerCode - ? { students: { some: { partnerCode: { equals: args.partnerCode, mode: 'insensitive' } } } } - : {} - ) - }, - select: { slackId: true } -}); + const students = await prisma.student.findMany({ + where: { + eventId: auth.eventId!, + projectEmails: args.emailId + ? { none: { emailSent: { emailId: args.emailId } } } + : { none: {} }, + slackId: { not: null }, + ...(args.partnerCode + ? { students: { some: { partnerCode: { equals: args.partnerCode, mode: 'insensitive' } } } } + : {} + ) + }, + select: { slackId: true } + }); -const slack = getSlackClientForEvent(event); + const slack = getSlackClientForEvent(event); -DEBUG(`Reminding ${students.length} students about ${args.emailId} email responses in ${args.channel}.`); -if (students.length > 0) { - await slack.chat.postMessage({ - blocks: [{ - type: 'section', - text: { - type: 'mrkdwn', - text: `[${event.name}]\n${args.intro}\n` + students.map(s => `- <@${s.slackId}>`).join(`\n`), - } - }], - channel: args.channel, - }); -} + DEBUG(`Reminding ${students.length} students about ${args.emailId} email responses in ${args.channel}.`); + if (students.length > 0) { + await slack.chat.postMessage({ + blocks: [{ + type: 'section', + text: { + type: 'mrkdwn', + text: `[${event.name}]\n${args.intro}\n` + students.map(s => `- <@${s.slackId}>`).join(`\n`), + } + }], + channel: args.channel, + }); + } } \ No newline at end of file diff --git a/src/email/postmark.ts b/src/email/postmark.ts index 4d83e0c..2e3c639 100644 --- a/src/email/postmark.ts +++ b/src/email/postmark.ts @@ -61,7 +61,7 @@ export async function processPostmarkInboundEmail(req: Request, res: Response) { DEBUG(`...project email addresses:`, myToEmails.join(',')); const projectId = myToEmails[0].split('@')[0].split('+')[0]; - const project = await prisma.project.findUniqueOrThrow({ + const project = await prisma.project.findUnique({ where: { id: projectId }, select: { id: true, @@ -88,7 +88,7 @@ export async function processPostmarkInboundEmail(req: Request, res: Response) { } const emailSentId = myToEmails[0].split('+')[1] || undefined; - const emailSent = !emailSentId ? undefined : await prisma.emailSent.findUniqueOrThrow({ + const emailSent = !emailSentId ? undefined : await prisma.emailSent.findUnique({ where: { id: emailSentId }, select: { id: true }, }); diff --git a/src/resolvers/Artifact.ts b/src/resolvers/Artifact.ts index 5a0bba4..05dd578 100644 --- a/src/resolvers/Artifact.ts +++ b/src/resolvers/Artifact.ts @@ -38,62 +38,62 @@ export class ArtifactResolver { }, }); - const artifactType = artifactTypeId && await this.prisma.artifactType.findFirstOrThrow({ - where: { - eventId: auth.eventId, - id: artifactTypeId, - }, - }); + const artifactType = artifactTypeId && await this.prisma.artifactType.findFirstOrThrow({ + where: { + eventId: auth.eventId, + id: artifactTypeId, + }, + }); - const groupArtifact = artifactType ? !artifactType.personType : _groupArtifact; + const groupArtifact = artifactType ? !artifactType.personType : _groupArtifact; - const nameTypeCriteria: Prisma.ArtifactWhereInput = artifactType - ? { artifactTypeId: { equals: artifactTypeId } } - : { name: { equals: name!, mode: 'insensitive' } }; + const nameTypeCriteria: Prisma.ArtifactWhereInput = artifactType + ? { artifactTypeId: { equals: artifactTypeId } } + : { name: { equals: name!, mode: 'insensitive' } }; - const whereCriteria: Prisma.ArtifactWhereInput = groupArtifact - ? { projectId: project.id } - : { - projectId: project.id, - [auth.personType === 'MENTOR' ? 'mentorId' : 'studentId']: auth.id, - }; + const whereCriteria: Prisma.ArtifactWhereInput = groupArtifact + ? { projectId: project.id } + : { + projectId: project.id, + [auth.personType === 'MENTOR' ? 'mentorId' : 'studentId']: auth.id, + }; - const nameData = artifactType ? artifactType.name : name!; + const nameData = artifactType ? artifactType.name : name!; - const connectData = groupArtifact - ? { projectId: project.id } - : { - projectId: project.id, - [auth.personType === 'MENTOR' ? 'mentorId' : 'studentId']: auth.id, - }; + const connectData = groupArtifact + ? { projectId: project.id } + : { + projectId: project.id, + [auth.personType === 'MENTOR' ? 'mentorId' : 'studentId']: auth.id, + }; - if(await this.prisma.artifact.count({ - where: { - ...nameTypeCriteria, - ...whereCriteria, + if (await this.prisma.artifact.count({ + where: { + ...nameTypeCriteria, + ...whereCriteria, + } + })) { + await this.prisma.artifact.updateMany({ + where: { + ...nameTypeCriteria, + ...whereCriteria, + }, + data: { + name: nameData, + link, + ...connectData, + }, + }); + } else { + await this.prisma.artifact.create({ + data: { + name: nameData, + link, + ...connectData, + }, + }) } - })) { - await this.prisma.artifact.updateMany({ - where: { - ...nameTypeCriteria, - ...whereCriteria, - }, - data: { - name: nameData, - link, - ...connectData, - }, - }); -} else { - await this.prisma.artifact.create({ - data: { - name: nameData, - link, - ...connectData, - }, - }) -} -return true; + return true; } } diff --git a/src/resolvers/FileType.ts b/src/resolvers/FileType.ts index c9e8e8b..09122d3 100644 --- a/src/resolvers/FileType.ts +++ b/src/resolvers/FileType.ts @@ -32,7 +32,7 @@ export class FileTypeResolver { async fileType( @Arg('id', () => String) id: string, ): Promise { - return this.prisma.fileType.findUniqueOrThrow({ + return this.prisma.fileType.findUnique({ where: { id }, }); } diff --git a/src/resolvers/Match.ts b/src/resolvers/Match.ts index f41d41a..0718daf 100644 --- a/src/resolvers/Match.ts +++ b/src/resolvers/Match.ts @@ -62,7 +62,7 @@ export class MatchResolver { const event = await this.prisma.event.findUniqueOrThrow({ where: { id: auth.eventId! } }); if (!event.matchPreferenceSubmissionOpen) throw new Error('Match preference submission is not open.'); - const student = await this.prisma.student.findUniqueOrThrow({ where: auth.toWhere() }); + const student = await this.prisma.student.findUnique({ where: auth.toWhere() }); if (!student || student.status !== StudentStatus.ACCEPTED) throw Error('You have not been accepted.'); if (student.skipPreferences) { throw new Error('You are not eligible to express project preferences. You will be matched manually in collaboration with your partner institution.'); diff --git a/src/resolvers/Mentor.ts b/src/resolvers/Mentor.ts index ae18b08..b7cc3af 100644 --- a/src/resolvers/Mentor.ts +++ b/src/resolvers/Mentor.ts @@ -42,7 +42,7 @@ export class MentorResolver { @Ctx() { auth }: Context, @Arg('where', () => IdOrUsernameInput, { nullable: true }) where?: IdOrUsernameInput, ): Promise { - const mentor = await this.prisma.mentor.findUniqueOrThrow({ + const mentor = await this.prisma.mentor.findUnique({ where: idOrUsernameOrAuthToUniqueWhere(auth, where), include: { event: true }, }); @@ -58,7 +58,7 @@ export class MentorResolver { @Ctx() { auth }: Context, @Arg('where', () => IdOrUsernameInput, { nullable: true }) where?: IdOrUsernameInput, ): Promise { - const currentMentor = await this.prisma.mentor.findUniqueOrThrow({ + const currentMentor = await this.prisma.mentor.findUnique({ where: idOrUsernameOrAuthToUniqueWhere(auth, where), select: { event: { select: { id: true } }, @@ -69,7 +69,7 @@ export class MentorResolver { if (!currentMentor) return null; if (!auth.isMentor && currentMentor.event.id !== auth.eventId) return null; - return await this.prisma.mentor.findFirstOrThrow({ + return await this.prisma.mentor.findFirst({ where: { eventId: { not: currentMentor?.event.id }, OR: [ @@ -100,7 +100,7 @@ export class MentorResolver { @Ctx() { auth }: Context, @Arg('data', () => MentorApplyInput) data: MentorApplyInput, ): Promise { - const event = await this.prisma.event.findUniqueOrThrow({ where: { id: auth.eventId } }); + const event = await this.prisma.event.findUnique({ where: { id: auth.eventId } }); if (!event) throw Error('Event does not exist.'); if (!eventAllowsApplicationMentor(event)) throw Error('Mentor applications are not open for this event.'); diff --git a/src/resolvers/Project.ts b/src/resolvers/Project.ts index 0847f96..f0ffed2 100644 --- a/src/resolvers/Project.ts +++ b/src/resolvers/Project.ts @@ -109,7 +109,7 @@ export class ProjectResolver { @Arg('project', () => String) project: string, @Arg('prUrl', () => String) prUrl: string, ): Promise { - const dbProject = await this.prisma.project.findUniqueOrThrow({ + const dbProject = await this.prisma.project.findUnique({ where: { id: project }, include: { mentors: { select: { id: true, username: true, givenName: true, surname: true, email: true } }, @@ -144,7 +144,7 @@ export class ProjectResolver { throw Error('You do not have permission to edit restricted fields.'); } - const dbProject = await this.prisma.project.findUniqueOrThrow({ + const dbProject = await this.prisma.project.findUnique({ where: { id: project }, include: { mentors: { select: { id: true, username: true, givenName: true, surname: true, email: true } }, diff --git a/src/resolvers/Review.ts b/src/resolvers/Review.ts index ea1e734..89660c2 100644 --- a/src/resolvers/Review.ts +++ b/src/resolvers/Review.ts @@ -40,7 +40,7 @@ export class ReviewResolver { }); if (results.length === 0) return null; const id = results[randInt(0, results.length)].id; - return this.prisma.student.findUniqueOrThrow({ where: { id } }); + return this.prisma.student.findUnique({ where: { id } }); } @Authorized(AuthRole.REVIEWER) @@ -156,7 +156,7 @@ export class ReviewResolver { @Arg('partnerContractData', () => GraphQLJSONObject, { nullable: true }) partnerContractData?: object, @Arg('timezone', () => String, { nullable: true }) timezone?: string, ): Promise { - const student = await this.prisma.student.findUniqueOrThrow({ + const student = await this.prisma.student.findUnique({ where: auth.toWhere(), select: { status: true, offerDate: true }, }); diff --git a/src/resolvers/ScheduledAnnouncement.ts b/src/resolvers/ScheduledAnnouncement.ts index 5b8d243..2c18b48 100644 --- a/src/resolvers/ScheduledAnnouncement.ts +++ b/src/resolvers/ScheduledAnnouncement.ts @@ -44,7 +44,7 @@ export class ScheduledAnnouncementResolver { @Arg('data', () => ScheduledAnnouncementCreateInput) data: ScheduledAnnouncementCreateInput, ): Promise { // Validate that the event exists and user has access - const event = await this.prisma.event.findUniqueOrThrow({ + const event = await this.prisma.event.findUnique({ where: { id: data.eventId }, select: { id: true }, }); @@ -70,7 +70,7 @@ export class ScheduledAnnouncementResolver { @Arg('data', () => ScheduledAnnouncementEditInput) data: ScheduledAnnouncementEditInput, ): Promise { // Validate that the announcement exists and user has access - const announcement = await this.prisma.scheduledAnnouncement.findUniqueOrThrow({ + const announcement = await this.prisma.scheduledAnnouncement.findUnique({ where: { id }, select: { id: true, eventId: true }, }); @@ -96,7 +96,7 @@ export class ScheduledAnnouncementResolver { @Arg('id', () => String) id: string, ): Promise { // Validate that the announcement exists and user has access - const announcement = await this.prisma.scheduledAnnouncement.findUniqueOrThrow({ + const announcement = await this.prisma.scheduledAnnouncement.findUnique({ where: { id }, select: { id: true, eventId: true }, }); diff --git a/src/resolvers/Standup.ts b/src/resolvers/Standup.ts index 87f69b4..891d883 100644 --- a/src/resolvers/Standup.ts +++ b/src/resolvers/Standup.ts @@ -62,7 +62,7 @@ export class StandupResolver { throw new Error('No permission to view this student\'s standups.'); } } else if (auth.isMentor) { - const id = auth.id ?? (await this.prisma.mentor.findUniqueOrThrow({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; + const id = auth.id ?? (await this.prisma.mentor.findUnique({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; const projectCount = await this.prisma.project.count({ where: { mentors: { some: { id: id } }, @@ -73,7 +73,7 @@ export class StandupResolver { throw new Error(`Cannot access this standup.`) } } else if (auth.isStudent) { - const id = auth.id ?? (await this.prisma.student.findUniqueOrThrow({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; + const id = auth.id ?? (await this.prisma.student.findUnique({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; if (standup.student.id !== id) { throw new Error(`Cannot access this standup.`); } diff --git a/src/resolvers/Student.ts b/src/resolvers/Student.ts index 0296429..6580c8f 100644 --- a/src/resolvers/Student.ts +++ b/src/resolvers/Student.ts @@ -41,7 +41,7 @@ export class StudentResolver { const event = await this.prisma.event.findUniqueOrThrow({ where: { id: auth.eventId! } }); const partner = auth.isPartner - ? await this.prisma.partner.findFirstOrThrow({ where: { partnerCode: auth.partnerCode, eventId: auth.eventId! } }) + ? await this.prisma.partner.findFirst({ where: { partnerCode: auth.partnerCode, eventId: auth.eventId! } }) : null; const partnerStudentCount = auth.isPartner @@ -200,7 +200,7 @@ export class StudentResolver { ): Promise { if (where) await validateStudentEvent(auth, where); - const student = await this.prisma.student.findUniqueOrThrow({ + const student = await this.prisma.student.findUnique({ where: idOrUsernameOrAuthToUniqueWhere(auth, where), include: { event: true }, }); @@ -234,8 +234,8 @@ export class StudentResolver { eventId: auth.eventId, }; - if (auth.type === AuthRole.APPLICANT_MENTOR) return (await this.prisma.mentor.findFirstOrThrow({ where }))?.id; - return (await this.prisma.student.findFirstOrThrow({ where }))?.id; + if (auth.type === AuthRole.APPLICANT_MENTOR) return (await this.prisma.mentor.findFirst({ where }))?.id; + return (await this.prisma.student.findFirst({ where }))?.id; } @Authorized([AuthRole.APPLICANT_STUDENT, AuthRole.APPLICANT_MENTOR]) @@ -303,14 +303,14 @@ export class StudentResolver { @Arg('data', () => StudentApplyInput) data: StudentApplyInput, ): Promise { if (!auth.username) throw Error('Username is required in token for student applicants.'); - const event = await this.prisma.event.findUniqueOrThrow({ where: { id: auth.eventId } }); + const event = await this.prisma.event.findUnique({ where: { id: auth.eventId } }); if (!event) throw Error('Event does not exist.'); if (!eventAllowsApplicationStudent(event)) throw Error('Student applications are not open for this event.'); let partnerData: Partial = { partnerCode: null }; if (data.partnerCode) { - const partner = await this.prisma.partner.findFirstOrThrow({ + const partner = await this.prisma.partner.findFirst({ where: { eventId: auth.eventId!, partnerCode: { equals: data.partnerCode.trim(), mode: 'insensitive' }, diff --git a/src/resolvers/Survey.ts b/src/resolvers/Survey.ts index b372503..ac01693 100644 --- a/src/resolvers/Survey.ts +++ b/src/resolvers/Survey.ts @@ -139,9 +139,9 @@ export class SurveyResolver { // Figure out ID if a username is provided let authorId: string | null = null; if (auth.isMentor) { - authorId = auth.id ?? (await this.prisma.mentor.findUniqueOrThrow({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; + authorId = auth.id ?? (await this.prisma.mentor.findUnique({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; } else { - authorId = auth.id ?? (await this.prisma.student.findUniqueOrThrow({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; + authorId = auth.id ?? (await this.prisma.student.findUnique({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; } if (!authorId) throw new Error(`Must provide an authorship token when creating a survey.`); @@ -200,7 +200,7 @@ export class SurveyResolver { throw new Error('No permission to view this student\'s survey responses.'); } } else if (auth.isMentor) { - const id = auth.id ?? (await this.prisma.mentor.findUniqueOrThrow({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; + const id = auth.id ?? (await this.prisma.mentor.findUnique({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; const projectCount = await this.prisma.project.count({ where: { mentors: { some: { id: id } }, @@ -214,7 +214,7 @@ export class SurveyResolver { throw new Error(`Cannot access this survey response.`) } } else if (auth.isStudent) { - const id = auth.id ?? (await this.prisma.student.findUniqueOrThrow({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; + const id = auth.id ?? (await this.prisma.student.findUnique({ where: { username_eventId: { username: auth.username!, eventId: auth.eventId! } } }))?.id!; if (surveyResponse.authorStudentId !== id && surveyResponse.studentId !== id) { throw new Error(`Cannot access this survey response.`); } diff --git a/src/resolvers/decorators.ts b/src/resolvers/decorators.ts index e02eaf1..5f3b717 100644 --- a/src/resolvers/decorators.ts +++ b/src/resolvers/decorators.ts @@ -15,7 +15,7 @@ export function MentorOnlySelf(argName: string): MethodDecorator { if (where) { let compare: { id?: string | null, username?: string | null } | null = where; if (!((auth.username && where.username) || (auth.id && where.id))) { - compare = await Container.get(PrismaClient).mentor.findUniqueOrThrow({ + compare = await Container.get(PrismaClient).mentor.findUnique({ where: where.toQuery(), select: { username: true, id: true }, }); @@ -38,7 +38,7 @@ export function StudentOnlySelf(argName: string): MethodDecorator { if (where) { let compare: { id?: string | null, username?: string | null } | null = where; if (!((auth.username && where.username) || (auth.id && where.id))) { - compare = await Container.get(PrismaClient).student.findUniqueOrThrow({ + compare = await Container.get(PrismaClient).student.findUnique({ where: where.toQuery(), select: { username: true, id: true }, }); diff --git a/src/search/getProjectMatches.ts b/src/search/getProjectMatches.ts index 66f1088..e11360a 100644 --- a/src/search/getProjectMatches.ts +++ b/src/search/getProjectMatches.ts @@ -80,7 +80,7 @@ function buildAffinityQuery(partner?: Partner | null): esb.MatchQuery | esb.Bool async function buildQueryFor(student: Student, tags: Tag[]): Promise { const prisma = Container.get(PrismaClient); const partner = student.partnerCode - ? await prisma.partner.findFirstOrThrow({ + ? await prisma.partner.findFirst({ where: { partnerCode: student.partnerCode, eventId: student.eventId, diff --git a/src/types/Artifact.ts b/src/types/Artifact.ts index a3c2f77..1791368 100644 --- a/src/types/Artifact.ts +++ b/src/types/Artifact.ts @@ -41,7 +41,7 @@ export class Artifact implements PrismaArtifact { async fetchStudent(): Promise { if (!this.studentId) return null; if (!this.student) { - this.student = (await Container.get(PrismaClient).student.findUniqueOrThrow({ + this.student = (await Container.get(PrismaClient).student.findUnique({ where: { id: this.studentId }, })) || undefined; } @@ -57,7 +57,7 @@ export class Artifact implements PrismaArtifact { async fetchMentor(): Promise { if (!this.mentorId) return null; if (!this.mentor) { - this.mentor = (await Container.get(PrismaClient).mentor.findUniqueOrThrow({ + this.mentor = (await Container.get(PrismaClient).mentor.findUnique({ where: { id: this.mentorId }, })) || undefined; } @@ -72,7 +72,7 @@ export class Artifact implements PrismaArtifact { @Field(() => Project, { name: 'project' }) async fetchProject(): Promise { if (!this.project) { - this.project = (await Container.get(PrismaClient).project.findUniqueOrThrow({ + this.project = (await Container.get(PrismaClient).project.findUnique({ where: { id: this.projectId }, })) || undefined; } diff --git a/src/types/Mentor.ts b/src/types/Mentor.ts index 92777ca..96abd36 100644 --- a/src/types/Mentor.ts +++ b/src/types/Mentor.ts @@ -86,7 +86,7 @@ export class Mentor implements PrismaMentor { @Field(() => Event, { name: 'event' }) async fetchEvent(): Promise { if (!this.event) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.eventId } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.eventId } }))!; } return this.event; diff --git a/src/types/Note.ts b/src/types/Note.ts index 36b1d69..1080dbb 100644 --- a/src/types/Note.ts +++ b/src/types/Note.ts @@ -39,7 +39,7 @@ export class Note implements PrismaNote { @Field(() => Student, { nullable: true, name: 'student' }) async fetchStudent(): Promise { if (!this.student) { - this.student = (await Container.get(PrismaClient).student.findUniqueOrThrow({ + this.student = (await Container.get(PrismaClient).student.findUnique({ where: { id: this.studentId }, })) || undefined; } @@ -54,7 +54,7 @@ export class Note implements PrismaNote { @Field(() => Event, { name: 'event' }) async fetchEvent(): Promise { if (!this.event) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.eventId } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.eventId } }))!; } return this.event; diff --git a/src/types/Partner.ts b/src/types/Partner.ts index 21d645f..91b7ff6 100644 --- a/src/types/Partner.ts +++ b/src/types/Partner.ts @@ -125,7 +125,7 @@ export class Partner { @Field(() => Event, { name: 'event' }) async fetchEvent(): Promise { if (!this.event) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.id } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.id } }))!; } return this.event; diff --git a/src/types/Project.ts b/src/types/Project.ts index 77ce567..e520b60 100644 --- a/src/types/Project.ts +++ b/src/types/Project.ts @@ -112,7 +112,7 @@ export class Project implements PrismaProject { @Field(() => Event, { name: 'event', nullable: true }) async fetchEvent(): Promise { if (!this.event && this.eventId) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.eventId } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.eventId } }))!; } return this.event ?? null; @@ -127,7 +127,7 @@ export class Project implements PrismaProject { async fetchRepository(): Promise { if (!this.repositoryId) return null; if (!this.repository) { - this.repository = (await Container.get(PrismaClient).repository.findUniqueOrThrow({ where: { id: this.repositoryId } }))!; + this.repository = (await Container.get(PrismaClient).repository.findUnique({ where: { id: this.repositoryId } }))!; } return this.repository; @@ -172,7 +172,7 @@ export class Project implements PrismaProject { this.affinePartner = ( await Container.get(PrismaClient) .partner - .findUniqueOrThrow({ where: { id: this.affinePartnerId } }) + .findUnique({ where: { id: this.affinePartnerId } }) )!; } diff --git a/src/types/Resource.ts b/src/types/Resource.ts index cdd8243..3a4cf79 100644 --- a/src/types/Resource.ts +++ b/src/types/Resource.ts @@ -47,7 +47,7 @@ export class Resource implements PrismaResource { @Field(() => Event, { name: 'event' }) async fetchEvent(): Promise { if (!this.event) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.eventId } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.eventId } }))!; } return this.event; diff --git a/src/types/Student.ts b/src/types/Student.ts index 61d45bb..3dc991a 100644 --- a/src/types/Student.ts +++ b/src/types/Student.ts @@ -255,7 +255,7 @@ export class Student implements PrismaStudent { @Field(() => Event, { name: 'event' }) async fetchEvent(): Promise { if (!this.event) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.eventId } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.eventId } }))!; } return this.event; @@ -274,7 +274,7 @@ export class Student implements PrismaStudent { if (!this.partnerCode) return null; if (!this.partner) { - this.partner = (await Container.get(PrismaClient).partner.findFirstOrThrow({ where: { partnerCode: this.partnerCode!, eventId: this.eventId } })) || null; + this.partner = (await Container.get(PrismaClient).partner.findFirst({ where: { partnerCode: this.partnerCode!, eventId: this.eventId } })) || null; } return this.partner; diff --git a/src/types/Survey.ts b/src/types/Survey.ts index c04aee6..4f3ccb0 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -111,7 +111,7 @@ export class Survey { @Field(() => Event, { name: 'event' }) async fetchEvent(): Promise { if (!this.event) { - this.event = (await Container.get(PrismaClient).event.findUniqueOrThrow({ where: { id: this.id } }))!; + this.event = (await Container.get(PrismaClient).event.findUnique({ where: { id: this.id } }))!; } return this.event; diff --git a/src/types/SurveyResponse.ts b/src/types/SurveyResponse.ts index c0cd657..8cf5773 100644 --- a/src/types/SurveyResponse.ts +++ b/src/types/SurveyResponse.ts @@ -46,7 +46,7 @@ export class SurveyResponse { @Field(() => SurveyOccurence, { name: 'surveyOccurence' }) async fetchSurveyOccurence(): Promise { if (!this.surveyOccurence) { - this.surveyOccurence = (await Container.get(PrismaClient).surveyOccurence.findUniqueOrThrow({ + this.surveyOccurence = (await Container.get(PrismaClient).surveyOccurence.findUnique({ where: { id: this.surveyOccurenceId }, include: { survey: true }, }))!; @@ -62,7 +62,7 @@ export class SurveyResponse { @Field(() => Student, { nullable: true, name: 'authorStudent' }) async fetchAuthorStudent(): Promise { if (!this.authorStudent && this.authorStudentId) { - this.authorStudent = (await Container.get(PrismaClient).student.findUniqueOrThrow({ + this.authorStudent = (await Container.get(PrismaClient).student.findUnique({ where: { id: this.authorStudentId }, })) || undefined; } @@ -77,7 +77,7 @@ export class SurveyResponse { @Field(() => Mentor, { nullable: true, name: 'authorMentor' }) async fetchAuthorMentor(): Promise { if (!this.authorMentor && this.authorMentorId) { - this.authorMentor = (await Container.get(PrismaClient).mentor.findUniqueOrThrow({ + this.authorMentor = (await Container.get(PrismaClient).mentor.findUnique({ where: { id: this.authorMentorId }, })) || undefined; } @@ -92,7 +92,7 @@ export class SurveyResponse { @Field(() => Student, { nullable: true, name: 'student' }) async fetchStudent(): Promise { if (!this.student && this.studentId) { - this.student = (await Container.get(PrismaClient).student.findUniqueOrThrow({ + this.student = (await Container.get(PrismaClient).student.findUnique({ where: { id: this.studentId }, })) || undefined; } @@ -107,7 +107,7 @@ export class SurveyResponse { @Field(() => Mentor, { nullable: true, name: 'mentor' }) async fetchMentor(): Promise { if (!this.mentor && this.mentorId) { - this.mentor = (await Container.get(PrismaClient).mentor.findUniqueOrThrow({ + this.mentor = (await Container.get(PrismaClient).mentor.findUnique({ where: { id: this.mentorId }, })) || undefined; } @@ -122,7 +122,7 @@ export class SurveyResponse { @Field(() => Project, { nullable: true, name: 'project' }) async fetchProject(): Promise { if (!this.project && this.projectId) { - this.project = (await Container.get(PrismaClient).project.findUniqueOrThrow({ + this.project = (await Container.get(PrismaClient).project.findUnique({ where: { id: this.projectId }, })) || undefined; } diff --git a/src/types/TagTrainingSubmission.ts b/src/types/TagTrainingSubmission.ts index 11a8703..4adb36e 100644 --- a/src/types/TagTrainingSubmission.ts +++ b/src/types/TagTrainingSubmission.ts @@ -36,12 +36,12 @@ export class TagTrainingSubmission implements PrismaTagTrainingSubmission { @Field(() => Tag, { name: 'tag' }) async fetchTag(): Promise { if (this.tag) return this.tag; - return Container.get(PrismaClient).tag.findUniqueOrThrow({ where: { id: this.tagId } }); + return Container.get(PrismaClient).tag.findUnique({ where: { id: this.tagId } }); } @Field(() => Student, { name: 'student' }) async fetchStudent(): Promise { if (this.tag) return this.student; - return Container.get(PrismaClient).tag.findUniqueOrThrow({ where: { id: this.tagId } }); + return Container.get(PrismaClient).tag.findUnique({ where: { id: this.tagId } }); } } diff --git a/src/utils/validateEvent.ts b/src/utils/validateEvent.ts index f92bbf4..3a7b971 100644 --- a/src/utils/validateEvent.ts +++ b/src/utils/validateEvent.ts @@ -5,14 +5,14 @@ import { IdOrUsernameInput, IdOrUsernameOrEmailInput } from "../inputs"; import { idOrUsernameOrEmailToUniqueWhere, idOrUsernameToUniqueWhere } from './idOrUsernameToUniqueWhere'; export async function validateStudentEvent(auth: AuthContext, arg: IdOrUsernameInput | IdOrUsernameOrEmailInput): Promise { - const student = await Container.get(PrismaClient).student.findUniqueOrThrow({ + const student = await Container.get(PrismaClient).student.findUnique({ where: idOrUsernameOrEmailToUniqueWhere(auth, arg), }); if (student && student.eventId !== auth.eventId) throw new Error('Student event does not match token event.'); } export async function validatePartnerStudent(auth: AuthContext, arg: IdOrUsernameInput | IdOrUsernameOrEmailInput): Promise { - const student = await Container.get(PrismaClient).student.findUniqueOrThrow({ + const student = await Container.get(PrismaClient).student.findUnique({ where: idOrUsernameOrEmailToUniqueWhere(auth, arg), }); if (student && auth.isPartner && @@ -23,21 +23,21 @@ export async function validatePartnerStudent(auth: AuthContext, arg: IdOrUsernam } export async function validateMentorEvent(auth: AuthContext, arg: IdOrUsernameInput | IdOrUsernameOrEmailInput): Promise { - const mentor = await Container.get(PrismaClient).mentor.findUniqueOrThrow({ + const mentor = await Container.get(PrismaClient).mentor.findUnique({ where: idOrUsernameOrEmailToUniqueWhere(auth, arg), }); if (mentor && mentor.eventId !== auth.eventId) throw new Error('Mentor event does not match token event.'); } export async function validateProjectEvent(auth: AuthContext, id: string): Promise { - const project = await Container.get(PrismaClient).project.findUniqueOrThrow({ + const project = await Container.get(PrismaClient).project.findUnique({ where: { id }, }); if (project && project.eventId !== auth.eventId) throw new Error('Project event does not match token event.'); } export async function validateSurveyEvent(auth: AuthContext, id: string): Promise { - const survey = await Container.get(PrismaClient).survey.findUniqueOrThrow({ + const survey = await Container.get(PrismaClient).survey.findUnique({ where: { id }, }); if (survey && survey.eventId !== auth.eventId) throw new Error('Survey event does not match token event.'); From 89216284edaaad47208c410675d393570528ab0b Mon Sep 17 00:00:00 2001 From: Hannah Cotterell Date: Tue, 6 Jan 2026 14:48:03 -0800 Subject: [PATCH 9/9] Catch if no event match in generateToken --- scripts/generateToken.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/generateToken.ts b/scripts/generateToken.ts index b5d81ad..4894ba2 100644 --- a/scripts/generateToken.ts +++ b/scripts/generateToken.ts @@ -10,6 +10,8 @@ async function main() { const event = await prisma.event.findUnique({ where: { id: eventId } }); + if (!event) throw new Error(`No event found with id ${eventId}`); + let token: string; if (role === 'admin') {