Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a139891
Completing REST endpoints for GET all and POST
Amormio25 Feb 11, 2026
7fe6eca
Fixing requested changes and renaming branch for all listing services
Amormio25 Feb 14, 2026
55a996b
Fixed requested changes for GET and POST, mainly filtering and pagina…
Amormio25 Feb 15, 2026
1b1248d
Adding logic for maintaining pagination, using validation schema for …
Amormio25 Feb 17, 2026
1430fd8
Refactoring code to follow product demo files and attempting to follo…
Amormio25 Feb 18, 2026
2531c80
Refactored listing API methods to ensure consistency with service-wid…
Amormio25 Feb 19, 2026
6c4024b
Fixing db connections and incorrect variables in listing api routes
Amormio25 Feb 20, 2026
d0a50ae
Merge branch 'main' into listings_svc in order to create listing API …
Amormio25 Feb 27, 2026
91bf583
Push commit to open PR for testing help
Amormio25 Mar 1, 2026
fa740b3
Created several tests for services and api layer, attempting to remov…
Amormio25 Apr 6, 2026
b460431
Updating jest config file to match main branch
Amormio25 Apr 7, 2026
fa0bc76
Updating listing schema and validation schema to follow front end upd…
Amormio25 Apr 11, 2026
9e03628
Merge branch 'main' into listings_svc
Amormio25 Apr 14, 2026
2b75513
Created GCS utility function for uploading image to free storage buck…
Amormio25 Apr 15, 2026
e7b0496
Updating tests and addressing ci/cd test fails
Amormio25 Apr 16, 2026
e7aee1d
Updating PUT handler to receive formData and adding more test cases t…
Amormio25 Apr 16, 2026
6864f70
Updated DELETE handler to return 200 to maintain a JSON response (204…
Amormio25 Apr 19, 2026
ea30d5d
Finishing all test cases for API endpoints
Amormio25 Apr 19, 2026
fa9959f
Merge branch 'main' into listings_svc
Amormio25 Apr 23, 2026
f5de9fb
Updating authorization checks in listing API
Amormio25 Apr 23, 2026
183f9a5
fixup! Updating authorization checks in listing API
Amormio25 Apr 23, 2026
5501d2e
Fixing failed tests by mocking RBAC in test file
Amormio25 Apr 24, 2026
3a9a6cf
Merge branch 'main' into listings_svc
Amormio25 Apr 24, 2026
f0e130a
Fix validation logic for params and form data handling
Amormio25 Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules/
dist/
.env.local
.DS_Store
.next
.env
coverage/
.next/
playwright-report/
.env
285 changes: 285 additions & 0 deletions app/api/listings/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import { NextResponse } from "next/server";
import { connectToDatabase } from "@/lib/mongoose";
import { z } from "zod";
import {
deleteListing,
getListing,
updateListing,
} from "@/services/listings/listings";
import { ListingInput } from "@/models/Listing";
import { uploadImage } from "@/lib/googleCloud";
import { getSession } from "@/lib/rbac";

const objectIdSchema = z
.string()
.regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId");
const listingValidationSchema = z
.object({
itemName: z.string(),
itemId: z.string(),
labName: z.string(),
labLocation: z.string(),
labId: z.string(),
imageUrls: z.array(z.string()),
quantityAvailable: z.number(),
expiryDate: z.date(),
description: z.string(),
price: z.number(),
status: z.enum(["ACTIVE", "INACTIVE"]),
condition: z.enum(["New", "Good", "Fair", "Poor"]),
hazardTags: z.array(
z.enum(["Physical", "Chemical", "Biological", "Other"])
),
})
.partial()
.strict();

/**
* Get a listing entry by ID
* @param id the ID of the listing to get
* ex req: GET /listings/001 HTTP/1.1
* @returns the listing as a JS object in a JSON response
*/
async function GET(request: Request, { params }: { params: { id: string } }) {
const { allowed, reason } = await getSession("inventory:view");
if (!allowed) {
return NextResponse.json(
{
success: false,
message: reason,
},
{ status: 403 }
);
}

try {
await connectToDatabase();
} catch {
return NextResponse.json(
{ success: false, message: "Error connecting to database." },
{ status: 500 }
);
}

const parsedId = objectIdSchema.safeParse(params.id);
if (!parsedId.success) {
return NextResponse.json(
{
success: false,
message: "Invalid ID format. Must be a valid MongoDB ObjectId.",
},
{ status: 400 }
);
}

try {
const listing = await getListing(parsedId.data); // don't need mongo doc features
if (!listing) {
return NextResponse.json(
{ success: false, message: "Listing not found." },
{ status: 404 }
);
}
return NextResponse.json({ success: true, data: listing }, { status: 200 });
} catch {
return NextResponse.json(
{ success: false, message: "Error occurred while retrieving listing." },
{ status: 500 }
);
}
}

/**
* Update a listing entry by ID
* @param id the ID of the listing to get as part of the path params
* @returns the updated listing as a JS object in a JSON response
*/
async function PUT(request: Request, { params }: { params: { id: string } }) {
const { allowed, reason } = await getSession("inventory:update");
if (!allowed) {
return NextResponse.json(
{
success: false,
message: reason,
},
{ status: 403 }
);
}

try {
await connectToDatabase();
} catch {
return NextResponse.json(
{ success: false, message: "Error connecting to database." },
{ status: 500 }
);
}

const parsedId = objectIdSchema.safeParse(params.id);
if (!parsedId.success) {
return NextResponse.json(
{
success: false,
message: "Invalid ID format. Must be a valid MongoDB ObjectId.",
},
{ status: 400 }
);
}

const formData = await request.formData();
const updateData: Partial<ListingInput> = {
...Object.fromEntries(formData.entries()),
};

// handle array fields
const hazardTags = formData.getAll("hazardTags");
if (hazardTags.length > 0) {
updateData.hazardTags = hazardTags as ListingInput["hazardTags"];
}

// type conversions
if (updateData.quantityAvailable !== undefined) {
updateData.quantityAvailable = Number(updateData.quantityAvailable);
}

if (updateData.price !== undefined) {
updateData.price = Number(updateData.price);
}

if (updateData.expiryDate !== undefined) {
updateData.expiryDate = new Date(
updateData.expiryDate as unknown as string
);
}

// handle image uploads if provided
const imageFiles = formData.getAll("images") as File[];

const parsedRequest = listingValidationSchema.safeParse(updateData);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upload happens before validation, lets validate first

if (!parsedRequest.success) {
return NextResponse.json(
{
success: false,
message: "Invalid request body.",
},
{ status: 400 }
);
}

if (imageFiles.length > 0) {
const imageUrls: string[] = [];

for (const imageFile of imageFiles) {
const buffer = Buffer.from(await imageFile.arrayBuffer());
const imageUrl = await uploadImage(buffer, imageFile.name);
imageUrls.push(imageUrl);
}

updateData.imageUrls = imageUrls;
}

try {
const updatedListing = await updateListing(
parsedId.data,
parsedRequest.data
);

if (!updatedListing) {
return NextResponse.json(
{
success: false,
message: "Listing not found",
},
{ status: 404 }
);
}

return NextResponse.json(
{
success: true,
data: updatedListing,
message: "Listing successfully updated.",
},
{ status: 200 }
);
} catch {
return NextResponse.json(
{
success: false,
message: "Error occurred while updating listing.",
},
{ status: 500 }
);
}
}

/**
* Delete a listing entry by ID
* @param id the ID of the listing to get as part of the path params
* @returns JSON response signaling the success of the listing deletion
*/
async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const { allowed, reason } = await getSession("inventory:delete");
if (!allowed) {
return NextResponse.json(
{
success: false,
message: reason,
},
{ status: 403 }
);
}

try {
await connectToDatabase();
} catch {
return NextResponse.json(
{ success: false, message: "Error connecting to database." },
{ status: 500 }
);
}

const parsedId = objectIdSchema.safeParse(params.id);
if (!parsedId.success) {
return NextResponse.json(
{
success: false,
message: "Invalid ID format. Must be a valid MongoDB ObjectId.",
},
{ status: 400 }
);
}

try {
const listing = await deleteListing(parsedId.data);
if (!listing) {
return NextResponse.json(
{
success: false,
message: "Listing not found",
},
{ status: 404 }
);
}
return NextResponse.json(
{
success: true,
message: "Listing successfully deleted.",
},
{ status: 200 }
);
} catch {
return NextResponse.json(
{
success: false,
message: "Error occurred while deleting listing.",
},
{ status: 500 }
);
}
}

export { GET, PUT, DELETE };
Loading
Loading