-
Notifications
You must be signed in to change notification settings - Fork 0
Completing Listing REST endpoints for GET all and POST #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 7fe6eca
Fixing requested changes and renaming branch for all listing services
Amormio25 55a996b
Fixed requested changes for GET and POST, mainly filtering and pagina…
Amormio25 1b1248d
Adding logic for maintaining pagination, using validation schema for …
Amormio25 1430fd8
Refactoring code to follow product demo files and attempting to follo…
Amormio25 2531c80
Refactored listing API methods to ensure consistency with service-wid…
Amormio25 6c4024b
Fixing db connections and incorrect variables in listing api routes
Amormio25 d0a50ae
Merge branch 'main' into listings_svc in order to create listing API …
Amormio25 91bf583
Push commit to open PR for testing help
Amormio25 fa740b3
Created several tests for services and api layer, attempting to remov…
Amormio25 b460431
Updating jest config file to match main branch
Amormio25 fa0bc76
Updating listing schema and validation schema to follow front end upd…
Amormio25 9e03628
Merge branch 'main' into listings_svc
Amormio25 2b75513
Created GCS utility function for uploading image to free storage buck…
Amormio25 e7b0496
Updating tests and addressing ci/cd test fails
Amormio25 e7aee1d
Updating PUT handler to receive formData and adding more test cases t…
Amormio25 6864f70
Updated DELETE handler to return 200 to maintain a JSON response (204…
Amormio25 ea30d5d
Finishing all test cases for API endpoints
Amormio25 fa9959f
Merge branch 'main' into listings_svc
Amormio25 f5de9fb
Updating authorization checks in listing API
Amormio25 183f9a5
fixup! Updating authorization checks in listing API
Amormio25 5501d2e
Fixing failed tests by mocking RBAC in test file
Amormio25 3a9a6cf
Merge branch 'main' into listings_svc
Amormio25 f0e130a
Fix validation logic for params and form data handling
Amormio25 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ node_modules/ | |
| dist/ | ||
| .env.local | ||
| .DS_Store | ||
| .next | ||
| .env | ||
| coverage/ | ||
| .next/ | ||
| playwright-report/ | ||
| .env | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| 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 }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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