Files
platform/apps/api/src/services/verifications.ts
T

194 lines
8.9 KiB
TypeScript

import { Activity, type ActivityVerifications, type ActivityMediaKeys } from "@blade-and-brawn/domain";
import type { Static } from "@sinclair/typebox";
import { t } from "elysia";
import type { Selectable } from "kysely";
import { db } from "../database/db";
import type { DB } from "../database/out/db";
export const VerificationStatusSchema = t.Union([
t.Literal("draft"),
t.Literal("submitted"),
t.Literal("in_review"),
t.Literal("action_requested"),
t.Literal("completed"),
]);
export type VerificationStatus = Static<typeof VerificationStatusSchema>;
const MEDIA_KEY_COLUMN = {
[Activity.BackSquat]: "media_key_back_squat",
[Activity.Deadlift]: "media_key_deadlift",
[Activity.BenchPress]: "media_key_bench_press",
[Activity.BroadJump]: "media_key_broad_jump",
[Activity.Run]: "media_key_run",
[Activity.ConeDrill]: "media_key_cone_drill",
} as const satisfies Record<Activity, keyof DB["verifications"]>;
const ALLOWED_FROM = {
update: ["draft", "action_requested"],
submit: ["draft"],
requestAction: ["submitted", "in_review"],
complete: ["submitted", "in_review"],
} satisfies Record<string, VerificationStatus[]>;
export class VerificationsService {
static readonly REQUIRED_MEDIA_ACTIVITIES = Object.values(Activity);
static readonly ALLOWED_MEDIA_RE = /^(video\/(mp4|quicktime|webm)|image\/(png|jpeg))$/;
static missingActivityMediaKeys(verification: Pick<Selectable<DB["verifications"]>, (typeof MEDIA_KEY_COLUMN)[Activity]>) {
return VerificationsService.REQUIRED_MEDIA_ACTIVITIES.filter((activity) => !verification[MEDIA_KEY_COLUMN[activity]]);
}
async create(assessmentId: string, activityMediaKeys?: ActivityMediaKeys) {
return await db.insertInto("verifications")
.values({
assessment_id: assessmentId,
status: "draft" satisfies VerificationStatus,
media_key_back_squat: activityMediaKeys?.[Activity.BackSquat] ?? null,
media_key_deadlift: activityMediaKeys?.[Activity.Deadlift] ?? null,
media_key_bench_press: activityMediaKeys?.[Activity.BenchPress] ?? null,
media_key_broad_jump: activityMediaKeys?.[Activity.BroadJump] ?? null,
media_key_run: activityMediaKeys?.[Activity.Run] ?? null,
media_key_cone_drill: activityMediaKeys?.[Activity.ConeDrill] ?? null,
})
.onConflict((oc) => oc.column("assessment_id").doNothing())
.returning(["id", "status"])
.executeTakeFirst();
}
async update(id: string, { assessmentId, activityMediaKeys = {} }: { assessmentId?: string, activityMediaKeys?: ActivityMediaKeys }) {
const replaced = (activity: Activity) => activityMediaKeys[activity] ? null : undefined;
return await db.updateTable("verifications")
.where("id", "=", id)
.where("status", "in", ALLOWED_FROM.update)
.set({
status: "draft" satisfies VerificationStatus,
submitted_at: null,
assessment_id: assessmentId,
media_key_back_squat: activityMediaKeys[Activity.BackSquat],
media_key_deadlift: activityMediaKeys[Activity.Deadlift],
media_key_bench_press: activityMediaKeys[Activity.BenchPress],
media_key_broad_jump: activityMediaKeys[Activity.BroadJump],
media_key_run: activityMediaKeys[Activity.Run],
media_key_cone_drill: activityMediaKeys[Activity.ConeDrill],
verf_back_squat: replaced(Activity.BackSquat),
verf_deadlift: replaced(Activity.Deadlift),
verf_bench_press: replaced(Activity.BenchPress),
verf_broad_jump: replaced(Activity.BroadJump),
verf_run: replaced(Activity.Run),
verf_cone_drill: replaced(Activity.ConeDrill),
})
.returning(["id", "status"])
.executeTakeFirst();
}
async get(id: string, accountId?: string) {
return await VerificationsService.baseQuery()
.where("verifications.id", "=", id)
.$if(accountId !== undefined, (qb) => qb.where("assessments.account_id", "=", accountId!))
.executeTakeFirst();
}
async list(opt: {
filter?: { status?: VerificationStatus },
limit?: number,
offset?: number,
} = {}) {
return await VerificationsService.baseQuery()
.$if(opt.filter?.status !== undefined, (qb) => qb
.where("verifications.status", "=", opt.filter!.status!)
)
.orderBy("verifications.submitted_at", (ob) => ob.desc().nullsLast())
.orderBy("verifications.created_at", "desc")
.$if(opt.limit !== undefined, (qb) => qb.limit(opt.limit!))
.$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!))
.execute();
}
async submit(id: string) {
return await db.updateTable("verifications")
.where("id", "=", id)
.where("status", "in", ALLOWED_FROM.submit)
.where((eb) => eb.and(
VerificationsService.REQUIRED_MEDIA_ACTIVITIES.map((activity) => eb(MEDIA_KEY_COLUMN[activity], "is not", null))
))
.set({ status: "submitted" satisfies VerificationStatus, submitted_at: new Date() })
.returning(["id", "status"])
.executeTakeFirst();
}
async requestAction(id: string, reviewerNotes: string | null, activityVerifications: ActivityVerifications) {
return await db.updateTable("verifications")
.where("id", "=", id)
.where("status", "in", ALLOWED_FROM.requestAction)
.set({
status: "action_requested" satisfies VerificationStatus,
reviewer_notes: reviewerNotes,
verf_back_squat: activityVerifications[Activity.BackSquat] ?? null,
verf_deadlift: activityVerifications[Activity.Deadlift] ?? null,
verf_bench_press: activityVerifications[Activity.BenchPress] ?? null,
verf_broad_jump: activityVerifications[Activity.BroadJump] ?? null,
verf_run: activityVerifications[Activity.Run] ?? null,
verf_cone_drill: activityVerifications[Activity.ConeDrill] ?? null,
})
.returning(["id", "status"])
.executeTakeFirst();
}
async complete(id: string, completedBy: string, activityVerifications: ActivityVerifications) {
return await db.updateTable("verifications")
.where("id", "=", id)
.where("status", "in", ALLOWED_FROM.complete)
.set({
status: "completed" satisfies VerificationStatus,
completed_at: new Date(),
completed_by: completedBy,
verf_back_squat: activityVerifications[Activity.BackSquat] ?? null,
verf_deadlift: activityVerifications[Activity.Deadlift] ?? null,
verf_bench_press: activityVerifications[Activity.BenchPress] ?? null,
verf_broad_jump: activityVerifications[Activity.BroadJump] ?? null,
verf_run: activityVerifications[Activity.Run] ?? null,
verf_cone_drill: activityVerifications[Activity.ConeDrill] ?? null,
})
.returning(["id", "status"])
.executeTakeFirst();
}
private static baseQuery() {
return db.selectFrom("verifications")
.innerJoin("assessments", "assessments.id", "verifications.assessment_id")
.select([
"verifications.id",
"verifications.assessment_id",
"verifications.status",
"verifications.created_at",
"verifications.submitted_at",
"verifications.completed_at",
"verifications.completed_by",
"verifications.reviewer_notes",
"verifications.media_key_back_squat",
"verifications.media_key_deadlift",
"verifications.media_key_bench_press",
"verifications.media_key_broad_jump",
"verifications.media_key_run",
"verifications.media_key_cone_drill",
"verifications.verf_back_squat",
"verifications.verf_deadlift",
"verifications.verf_bench_press",
"verifications.verf_broad_jump",
"verifications.verf_run",
"verifications.verf_cone_drill",
"assessments.name",
"assessments.gender",
"assessments.age",
"assessments.weight",
"assessments.perf_back_squat",
"assessments.perf_deadlift",
"assessments.perf_bench_press",
"assessments.perf_broad_jump",
"assessments.perf_run",
"assessments.perf_cone_drill",
]);
}
}