diff options
author | Linnnus <[email protected]> | 2025-02-19 18:37:33 +0100 |
---|---|---|
committer | Linnnus <[email protected]> | 2025-02-19 18:37:51 +0100 |
commit | f7a6244cc1a8f39ad44186a4da6b743ab6821d51 (patch) | |
tree | 54e63d1509f56caaac542a4548325f9716d42069 /app/src/lib | |
parent | 80a432fbda4ca2f35286197c636ad2a733ca4b5a (diff) |
Add assignment submission
Diffstat (limited to 'app/src/lib')
-rw-r--r-- | app/src/lib/common/assignments.ts | 32 | ||||
-rw-r--r-- | app/src/lib/server/assignments.ts | 106 | ||||
-rw-r--r-- | app/src/lib/server/s3.ts | 16 | ||||
-rw-r--r-- | app/src/lib/server/sessions.ts | 4 |
4 files changed, 133 insertions, 25 deletions
diff --git a/app/src/lib/common/assignments.ts b/app/src/lib/common/assignments.ts new file mode 100644 index 0000000..79553cd --- /dev/null +++ b/app/src/lib/common/assignments.ts @@ -0,0 +1,32 @@ +/** A row from the `cemetary_plot` table. */ +export interface CemetaryPlot { + id: number; + address: string; + ownerId: number; + //assignmentInterval: Date; +} + +/** Corresponds to `assignment_state`. */ +export type AssignmentState = "AWAITING_GARDENER_NOTIFICATION" + | "AWAITING_FINISH" + | "AWAITING_WATERMARKING" + | "AWAITING_OWNER_NOTIFICATION" + | "DONE" + ; + +/** A row from the `assignments` table. */ +export interface Assignment { + id: number; + gardenerId: number; + cemetaryPlotId: number; + date: Date; + state: AssignmentState; +} + + +// FIXME: We have ORM at home. A more clearly defined (OOP) model layer. +/** Checks whether the state of the assignment allows it to be "finished". */ +export function canBeFinished(assignment: Assignment): boolean { + return (assignment.state === "AWAITING_GARDENER_NOTIFICATION" || + assignment.state === "AWAITING_FINISH") +} diff --git a/app/src/lib/server/assignments.ts b/app/src/lib/server/assignments.ts index 7622408..787865c 100644 --- a/app/src/lib/server/assignments.ts +++ b/app/src/lib/server/assignments.ts @@ -1,20 +1,6 @@ -import type pg from "pg"; - -/** A row from the `cemetary_plot` table. */ -export interface CemetaryPlot { - id: number; - address: string; - ownerId: number; - //assignmentInterval: Date; -} - -/** A row from the `assignments` table. */ -export interface Assignment { - id: number; - gardenerId: number; - cemetaryPlotId: number; - date: Date; -} +import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3"; /*=;* / +import type { ClientBase } from "pg"; +import type { CemetaryPlot, Assignment, AssignmentState } from "../common/assignments"; /** * Retrieves all assignments for the given user. @@ -22,7 +8,7 @@ export interface Assignment { * @param dbConn Connection to database. * @param userId ID used to identify user. */ -export async function getAssignments(dbConn: pg.ClientBase, userId: number): Promise<Assignment[]> { +export async function getAssignments(dbConn: ClientBase, userId: number): Promise<Assignment[]> { const result = await dbConn.query( "SELECT * FROM assignments WHERE gardener_id = $1 ORDER BY date", [userId], @@ -34,6 +20,7 @@ export async function getAssignments(dbConn: pg.ClientBase, userId: number): Pro gardenerId: r.gardener_id, cemetaryPlotId: r.cemetary_plot_id, date: r.date, + state: r.state, }) satisfies Assignment, ); } @@ -46,7 +33,7 @@ type GetAssignmentResult = * Retrieves a specfic assignment, along with relevant cemetary plot. */ export async function getAssignmentAndCemetaryById( - dbConn: pg.ClientBase, + dbConn: ClientBase, assignmentId: number, ): Promise<GetAssignmentResult> { const queryText = `SELECT @@ -54,6 +41,7 @@ export async function getAssignmentAndCemetaryById( a.gardener_id, a.cemetary_plot_id, a.date, + a.state, c.id, c.address, c.owner_id, @@ -73,12 +61,84 @@ export async function getAssignmentAndCemetaryById( gardenerId: result.rows[0][1], cemetaryPlotId: result.rows[0][2], date: result.rows[0][3], + state: result.rows[0][4], }; const cemetaryPlot: CemetaryPlot = { - id: result.rows[0][4], - address: result.rows[0][5], - ownerId: result.rows[0][6], - //assignmentInterval: result.rows[0][7], + id: result.rows[0][5], + address: result.rows[0][6], + ownerId: result.rows[0][7], + //assignmentInterval: result.rows[0][8], }; return { assignment, cemetaryPlot }; } + +export interface FinishAssignmentArgs { + images: { bytes: Uint8Array; name: string }[]; + note?: string; + assignmentId: number; +} + +// TODO: Error recovery. +export async function finishAssignment( + dbConn: ClientBase, + s3Client: S3Client, + { images, note, assignmentId }: FinishAssignmentArgs, +): Promise<void> { + // Upload to S3, returning path + // FIXME: Should be factored out? + const uploadPromises = images.map(async (image) => { + const key = generateImageKey(); + + const cmd = new PutObjectCommand({ + Bucket: "images", + Key: key, + Body: image.bytes, + IfNoneMatch: "*", // Error, in case of key collision. + }); + + await s3Client.send(cmd); + return { ...image, key }; + }); + const uploadedImages = await Promise.all(uploadPromises); + + // TODO: Add beanstalkd job + + await dbConn.query("BEGIN"); + + try { + // Create 'images' row for each image. + // FIXME: Apparently node-pg doesn't have an equivalent to Python's `insert_many`?? + for (const image of uploadedImages) { + dbConn.query({ + name: "insert-assignment-image", + text: "INSERT INTO images(s3_path, original_filename, assignment_id) VALUES ($1, $2, $3)", + values: [image.key, image.name, assignmentId], + }); + } + + // Update the assingment's state. + dbConn.query( + `UPDATE + assignments + SET + state = 'AWAITING_WATERMARKING' :: assignment_state, + note = $1 + WHERE + id = $2`, + [note, assignmentId], + ); + + dbConn.query("COMMIT"); + } catch (err) { + // We should probably try to delete S3 objects. + + // We should probably try to delete the job. + + await dbConn.query("ROLLBACK"); + throw err; + } +} + +function generateImageKey(): string { + return crypto.randomUUID(); +} diff --git a/app/src/lib/server/s3.ts b/app/src/lib/server/s3.ts new file mode 100644 index 0000000..a1a28fa --- /dev/null +++ b/app/src/lib/server/s3.ts @@ -0,0 +1,16 @@ +import { S3Client } from "@aws-sdk/client-s3"; + +// We would obviously read from .env in prod, but it's an annoying indirection for this demo. +export function getS3Client(): S3Client { + const client = new S3Client({ + endpoint: "http://localhost:9000", + region: "us-east-1", // Required, but ignored for local usage. + credentials: { + accessKeyId: "minioadmin", + secretAccessKey: "minioadmin", + }, + forcePathStyle: true, // Required for local service since we obv. can't use subdomains. + }); + + return client; +} diff --git a/app/src/lib/server/sessions.ts b/app/src/lib/server/sessions.ts index 0ca46e0..69a8b46 100644 --- a/app/src/lib/server/sessions.ts +++ b/app/src/lib/server/sessions.ts @@ -88,7 +88,7 @@ export async function validateSessionToken( WHERE token = $1;`, [token], ); - console.debug(result); + //console.debug(result); if (result.rowCount === 0) { return { session: null, user: null }; } @@ -105,7 +105,7 @@ export async function validateSessionToken( lastName: result.rows[0].last_name, role: result.rows[0].role, }; - console.debug("Session with token %s: %o, %o", token, session, user); + //console.debug("Session with token %s: %o, %o", token, session, user); // Step 2. const now = Date.now(); |