From a1398913ba5582c507de72bebcd85ee5fb727f64 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Tue, 10 Feb 2026 22:06:00 -0800 Subject: [PATCH 01/20] Completing REST endpoints for GET all and POST --- app/api/listings/[id]/route.ts | 0 app/api/listings/route.ts | 97 ++++++++++++++++++++++++++++++++++ models/Listing.ts | 4 +- 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 app/api/listings/[id]/route.ts create mode 100644 app/api/listings/route.ts diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts new file mode 100644 index 0000000..0654e9d --- /dev/null +++ b/app/api/listings/route.ts @@ -0,0 +1,97 @@ +import { NextResponse } from "next/server"; +import { connectToDatabase } from "@/lib/mongoose"; +import { z } from "zod"; +import Listing from "@/models/Listing"; + +// test comment +// ask about whether users should provide _id, that seems more like a +// server task, the rest they should be able to +const listingValidationSchema = z.object({ + _id: z.string().min(1), + itemId: z.string().min(1), + labId: z.string().min(1), + quantityAvailable: z.number().min(0), // can list items with 0 quantity? + createdAt: z + .string() + // could possibly change to MM-DD-YYYY + .regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date format. Expected YYYY-MM-DD."), +}); + +async function connect() { + try { + await connectToDatabase(); + } catch { + return NextResponse.json( + { success: false, message: "Error connecting to database" }, + { status: 500 } + ); + } +} + +// get all +async function GET() { + const connectionResponse = await connect(); + if (connectionResponse) return connectionResponse; + + try { + const listings = await Listing.find(); + return NextResponse.json( + { success: true, data: listings }, + { status: 200 } + ); + } catch (error) { + return NextResponse.json( + { success: false, message: "Error occurred while retrieving listings." }, + { status: 500 } + ); + } +} + +// post all +async function POST(request: Request) { + const connectionResponse = await connect(); + if (connectionResponse) return connectionResponse; + + // assuming frontend sends req with content-type set to app/json + // content type automatically set as app/json + const body = await request.json(); + const parsedBody = listingValidationSchema.safeParse(body); + + if (!parsedBody.success) { + return NextResponse.json( + { + success: false, + message: "Invalid request body.", + // error: parsedBody.error.format(), don't expose error? + }, + { status: 400 } + ); + } + + try { + const listing = await Listing.create(parsedBody.data); + return NextResponse.json( + { + success: true, + message: "Successfully created new listing.", + data: listing, + }, + { status: 201, headers: { Location: `/app/listings/${listing._id}` } } + // resource will be retrievable at this url + ); + } catch (error: any) { + if (error.code === 11000) { + return NextResponse.json( + // don't send mongo's error - exposes design info + { success: false, message: "This listing already exists." }, + { status: 409 } + ); + } + return NextResponse.json( + { success: false, message: "Error occurred while creating new listing." }, + { status: 500 } + ); + } +} + +export { GET, POST }; diff --git a/models/Listing.ts b/models/Listing.ts index 2de73be..904777d 100644 --- a/models/Listing.ts +++ b/models/Listing.ts @@ -23,5 +23,5 @@ listingSchema.index( { unique: true } ); -const listing = mongoose.models.Listing || model("Listing", listingSchema); -export default listing; +const Listing = mongoose.models.Listing || model("Listing", listingSchema); +export default Listing; From 7fe6ecad48fb6d891f46e2c5adae45e1af852ef2 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Fri, 13 Feb 2026 18:01:11 -0800 Subject: [PATCH 02/20] Fixing requested changes and renaming branch for all listing services --- app/api/listings/route.ts | 14 ++++++-------- models/Listing.ts | 1 - 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 0654e9d..fa3e302 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -3,20 +3,17 @@ import { connectToDatabase } from "@/lib/mongoose"; import { z } from "zod"; import Listing from "@/models/Listing"; -// test comment -// ask about whether users should provide _id, that seems more like a -// server task, the rest they should be able to const listingValidationSchema = z.object({ - _id: z.string().min(1), itemId: z.string().min(1), labId: z.string().min(1), - quantityAvailable: z.number().min(0), // can list items with 0 quantity? + quantityAvailable: z.number().min(1), createdAt: z .string() // could possibly change to MM-DD-YYYY .regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date format. Expected YYYY-MM-DD."), }); +// helper method to verify connection async function connect() { try { await connectToDatabase(); @@ -28,7 +25,8 @@ async function connect() { } } -// get all +// GET: Return all listings stored in DB +// input: get request with no query string for id async function GET() { const connectionResponse = await connect(); if (connectionResponse) return connectionResponse; @@ -47,7 +45,8 @@ async function GET() { } } -// post all +// POST: Create a new listing in DB +// input: post request with json data in body async function POST(request: Request) { const connectionResponse = await connect(); if (connectionResponse) return connectionResponse; @@ -77,7 +76,6 @@ async function POST(request: Request) { data: listing, }, { status: 201, headers: { Location: `/app/listings/${listing._id}` } } - // resource will be retrievable at this url ); } catch (error: any) { if (error.code === 11000) { diff --git a/models/Listing.ts b/models/Listing.ts index 904777d..8e365e3 100644 --- a/models/Listing.ts +++ b/models/Listing.ts @@ -6,7 +6,6 @@ const MONGODB_URI = process.env.DATABASE_URL!; mongoose.connect(MONGODB_URI); const listingSchema = new Schema({ - _id: { type: String, required: true }, itemId: { type: String, required: true }, labId: { type: String, required: true }, quantityAvailable: { type: Number, required: true }, From 55a996b0495e3a3a9e8f854f23ba3182a55a7d7c Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Sun, 15 Feb 2026 15:42:37 -0800 Subject: [PATCH 03/20] Fixed requested changes for GET and POST, mainly filtering and pagination for GET requests --- app/api/listings/route.ts | 56 +++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index fa3e302..2a1e137 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -1,17 +1,8 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; -import { z } from "zod"; import Listing from "@/models/Listing"; -const listingValidationSchema = z.object({ - itemId: z.string().min(1), - labId: z.string().min(1), - quantityAvailable: z.number().min(1), - createdAt: z - .string() - // could possibly change to MM-DD-YYYY - .regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid date format. Expected YYYY-MM-DD."), -}); +/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ // helper method to verify connection async function connect() { @@ -25,14 +16,40 @@ async function connect() { } } -// GET: Return all listings stored in DB -// input: get request with no query string for id -async function GET() { +// GET: Return a number of filtered listings stored in db +// input: req for an amount of certain listings +// (ex: /listings/?labId=3&page=2&limit=5) +// output: 10 possibly filtered listings from the db +async function GET(request: Request) { const connectionResponse = await connect(); if (connectionResponse) return connectionResponse; + const { searchParams } = new URL(request.url); + const labId = searchParams.get("labId"); + const itemId = searchParams.get("itemId"); + + /* FILTERS */ + // build query obj to filter by lab/item id if filters not null + const query: any = {}; + if (labId) query.labId = labId; + if (itemId) query.itemId = itemId; + + /* PAGINATION */ + // default to 10 listings per page + const pageParam = parseInt(searchParams.get("page") || "1"); + const limitParam = parseInt(searchParams.get("limit") || "10"); + + // page must be >= 1 if given + const page = isNaN(pageParam) || pageParam < 1 ? 1 : pageParam; + + const MAX_LIMIT = 20; // inquire about this in the future + const limit = + isNaN(limitParam) || limitParam < 1 ? 10 : Math.min(limitParam, MAX_LIMIT); + + const skip = (page - 1) * limit; + try { - const listings = await Listing.find(); + const listings = await Listing.find(query).skip(skip).limit(limit); return NextResponse.json( { success: true, data: listings }, { status: 200 } @@ -53,22 +70,21 @@ async function POST(request: Request) { // assuming frontend sends req with content-type set to app/json // content type automatically set as app/json - const body = await request.json(); - const parsedBody = listingValidationSchema.safeParse(body); - - if (!parsedBody.success) { + let body; + try { + body = await request.json(); + } catch { return NextResponse.json( { success: false, message: "Invalid request body.", - // error: parsedBody.error.format(), don't expose error? }, { status: 400 } ); } try { - const listing = await Listing.create(parsedBody.data); + const listing = await Listing.create({ ...body, createdAt: new Date() }); return NextResponse.json( { success: true, From 1b1248d137152d9025f6097c6cdd9f54987b9e82 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Mon, 16 Feb 2026 17:23:11 -0800 Subject: [PATCH 04/20] Adding logic for maintaining pagination, using validation schema for now, created indexing for filters in schema file --- app/api/listings/route.ts | 44 ++++++++++++++++++++++++++++++--------- models/Listing.ts | 20 +++++++++++------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 2a1e137..f986214 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -1,8 +1,14 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; +import { z } from "zod"; import Listing from "@/models/Listing"; /* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ +const listingValidationSchema = z.object({ + itemId: z.string().min(1), + labId: z.string().min(1), + quantityAvailable: z.number().min(1), +}); // helper method to verify connection async function connect() { @@ -35,13 +41,10 @@ async function GET(request: Request) { if (itemId) query.itemId = itemId; /* PAGINATION */ - // default to 10 listings per page const pageParam = parseInt(searchParams.get("page") || "1"); const limitParam = parseInt(searchParams.get("limit") || "10"); - // page must be >= 1 if given const page = isNaN(pageParam) || pageParam < 1 ? 1 : pageParam; - const MAX_LIMIT = 20; // inquire about this in the future const limit = isNaN(limitParam) || limitParam < 1 ? 10 : Math.min(limitParam, MAX_LIMIT); @@ -49,9 +52,26 @@ async function GET(request: Request) { const skip = (page - 1) * limit; try { - const listings = await Listing.find(query).skip(skip).limit(limit); + const [listings, total] = await Promise.all([ + Listing.find(query) + .sort({ createdAt: -1 }) // sort from newest to oldest + .skip(skip) + .limit(limit) + .lean(), // return js obj instead of mongoose documents + Listing.countDocuments(query), // total listings for this query + ]); + return NextResponse.json( - { success: true, data: listings }, + { + success: true, + data: listings, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }, { status: 200 } ); } catch (error) { @@ -70,10 +90,10 @@ async function POST(request: Request) { // assuming frontend sends req with content-type set to app/json // content type automatically set as app/json - let body; - try { - body = await request.json(); - } catch { + const body = await request.json(); + const parsedBody = listingValidationSchema.safeParse(body); + + if (!parsedBody.success) { return NextResponse.json( { success: false, @@ -84,7 +104,11 @@ async function POST(request: Request) { } try { - const listing = await Listing.create({ ...body, createdAt: new Date() }); + const listing = await Listing.create({ + ...parsedBody.data, + // could possibly have {timestamps:true in schema to remove date stamp here} + createdAt: new Date(), + }); return NextResponse.json( { success: true, diff --git a/models/Listing.ts b/models/Listing.ts index 8e365e3..ac8d2b8 100644 --- a/models/Listing.ts +++ b/models/Listing.ts @@ -13,14 +13,18 @@ const listingSchema = new Schema({ createdAt: { type: Date, required: true }, }); -listingSchema.index( - { - itemId: 1, - labId: 1, - createdAt: 1, - }, - { unique: true } -); +// listingSchema.index( +// { +// itemId: 1, +// labId: 1, +// createdAt: 1, +// }, +// { unique: true } +// ); + +// for filtering +listingSchema.index({ labId: 1, createdAt: -1 }); +listingSchema.index({ itemId: 1, createdAt: -1 }); const Listing = mongoose.models.Listing || model("Listing", listingSchema); export default Listing; From 1430fd850ec1434e80a35ff59780571a593d3a82 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Tue, 17 Feb 2026 21:44:26 -0800 Subject: [PATCH 05/20] Refactoring code to follow product demo files and attempting to follow requested changes --- app/api/listings/[id]/route.ts | 74 ++++++++++++++++++++ app/api/listings/route.ts | 53 +++++--------- models/Listing.ts | 57 ++++++++++----- services/listings/listings.ts | 124 +++++++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 53 deletions(-) create mode 100644 services/listings/listings.ts diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index e69de29..6ff10e0 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; +import { connectToDatabase } from "@/lib/mongoose"; +import { z } from "zod"; +import Listing from "@/models/Listing"; +import mongoose from "mongoose"; + +/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ +const listingValidationSchema = z.object({ + itemId: z.string().min(1), + labId: z.string().min(1), + quantityAvailable: z.number().min(1), +}); + +// helper method to verify connection +async function connect() { + try { + await connectToDatabase(); + } catch { + return NextResponse.json( + { success: false, message: "Error connecting to database." }, + { status: 500 } + ); + } +} + +// GET: Return a single listing in the db +// input: req for specific listing with given id +// (ex: /listings/001) +// output: json response with the listing as a JS object if found +async function GET(request: Request) { + const connectionResponse = await connect(); + if (connectionResponse) return connectionResponse; + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id || !mongoose.isValidObjectId(id)) { + return NextResponse.json( + { + success: false, + message: "Invalid ID format. Must be a valid MongoDB ObjectId.", + }, + { status: 400 } + ); + } + + try { + const listing = await Listing.findById(id).lean(); // 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 } + ); + } +} + +async function PUT(request: Request) { + const connectionResponse = await connect(); + if (connectionResponse) return connectionResponse; +} + +async function DELETE(request: Request) { + const connectionResponse = await connect(); + if (connectionResponse) return connectionResponse; +} + +export { GET, PUT, DELETE }; diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index f986214..39eea91 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -2,6 +2,13 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; import { z } from "zod"; import Listing from "@/models/Listing"; +import { + getFilteredListings, + getListing, + addListing, + updateListing, + deleteListing, +} from "@/services/listings/listings"; /* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ const listingValidationSchema = z.object({ @@ -31,50 +38,28 @@ async function GET(request: Request) { if (connectionResponse) return connectionResponse; const { searchParams } = new URL(request.url); - const labId = searchParams.get("labId"); - const itemId = searchParams.get("itemId"); - - /* FILTERS */ - // build query obj to filter by lab/item id if filters not null - const query: any = {}; - if (labId) query.labId = labId; - if (itemId) query.itemId = itemId; - - /* PAGINATION */ - const pageParam = parseInt(searchParams.get("page") || "1"); - const limitParam = parseInt(searchParams.get("limit") || "10"); - - const page = isNaN(pageParam) || pageParam < 1 ? 1 : pageParam; - const MAX_LIMIT = 20; // inquire about this in the future - const limit = - isNaN(limitParam) || limitParam < 1 ? 10 : Math.min(limitParam, MAX_LIMIT); - - const skip = (page - 1) * limit; + const labId = searchParams.get("labId") || undefined; + const itemId = searchParams.get("itemId") || undefined; + const page = parseInt(searchParams.get("page") || "1"); + const limit = parseInt(searchParams.get("limit") || "10"); try { - const [listings, total] = await Promise.all([ - Listing.find(query) - .sort({ createdAt: -1 }) // sort from newest to oldest - .skip(skip) - .limit(limit) - .lean(), // return js obj instead of mongoose documents - Listing.countDocuments(query), // total listings for this query - ]); + const { listings, pagination } = await getFilteredListings({ + labId, + itemId, + page, + limit, + }); return NextResponse.json( { success: true, data: listings, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - }, + pagination, }, { status: 200 } ); - } catch (error) { + } catch { return NextResponse.json( { success: false, message: "Error occurred while retrieving listings." }, { status: 500 } diff --git a/models/Listing.ts b/models/Listing.ts index ac8d2b8..585b2a4 100644 --- a/models/Listing.ts +++ b/models/Listing.ts @@ -1,30 +1,49 @@ import mongoose from "mongoose"; +import { + HydratedDocument, + InferSchemaType, + Model, + Schema, + model, + models, +} from "mongoose"; -const { Schema, model } = mongoose; const MONGODB_URI = process.env.DATABASE_URL!; - mongoose.connect(MONGODB_URI); -const listingSchema = new Schema({ - itemId: { type: String, required: true }, - labId: { type: String, required: true }, - quantityAvailable: { type: Number, required: true }, - status: { type: String, enum: ["ACTIVE", "INACTIVE"], required: true }, - createdAt: { type: Date, required: true }, -}); +const transformDocument = (_: unknown, ret: Record) => { + ret.id = ret._id?.toString(); + delete ret._id; + return ret; +}; // for properly handling toObject() or toJSON() and stringifying id -// listingSchema.index( -// { -// itemId: 1, -// labId: 1, -// createdAt: 1, -// }, -// { unique: true } -// ); +const listingSchema = new Schema( + { + itemId: { type: String, required: true }, + labId: { type: String, required: true }, + quantityAvailable: { type: Number, required: true }, + status: { type: String, enum: ["ACTIVE", "INACTIVE"], required: true }, + createdAt: { type: Date, required: true }, + }, + { + toJSON: { virtuals: true, versionKey: false, transform: transformDocument }, + toObject: { + virtuals: true, + versionKey: false, + transform: transformDocument, + }, + } +); // for filtering listingSchema.index({ labId: 1, createdAt: -1 }); listingSchema.index({ itemId: 1, createdAt: -1 }); -const Listing = mongoose.models.Listing || model("Listing", listingSchema); -export default Listing; +export type ListingInput = InferSchemaType; +export type Listing = ListingInput & { id: string }; +export type ListingDocument = HydratedDocument; + +const ListingModel: Model = + (models.Listing as Model) || + model("Listing", listingSchema); +export default ListingModel; diff --git a/services/listings/listings.ts b/services/listings/listings.ts new file mode 100644 index 0000000..a84c670 --- /dev/null +++ b/services/listings/listings.ts @@ -0,0 +1,124 @@ +import type { HydratedDocument } from "mongoose"; +import { connectToDatabase } from "@/lib/mongoose"; +import ListingModel, { Listing, ListingInput } from "@/models/Listing"; + +type ListingDocument = HydratedDocument; +const toListing = (doc: ListingDocument): Listing => doc.toObject(); + +interface FilterParams { + labId?: string; + itemId?: string; + page: number; + limit: number; +} + +/** + * Get all listing entries + * @returns array of listings as JS objects + */ +// async function getListings(): Promise { +// await connectToDatabase(); +// const listings = await ListingModel.find().exec(); +// return listings.map((listing) => toListing(listing)); +// } + +/** + * Get filtered listing entries + * @returns array of listings as JS objects + */ +async function getFilteredListings({ + labId, + itemId, + page, + limit, +}: FilterParams) { + const query: any = {}; + if (labId) query.labId = labId; + if (itemId) query.itemId = itemId; + + const MAX_LIMIT = 20; // inquire about this in the future + const validPage = isNaN(page) || page < 1 ? 1 : page; + const validLimit = + isNaN(limit) || limit < 1 ? 10 : Math.min(limit, MAX_LIMIT); + const skip = (page - 1) * limit; + + const [listings, total] = await Promise.all([ + ListingModel.find(query) + .sort({ createdAt: -1 }) // Sort from newest to oldest + .skip(skip) + .limit(validLimit) + .exec(), // toListing handles doc to JS object, so lean unncessary + ListingModel.countDocuments(query), + ]); + + return { + listings: listings.map((listing) => toListing(listing)), + pagination: { + page: validPage, + limit: validLimit, + total, + totalPages: Math.ceil(total / validLimit), + }, + }; +} + +/** + * Get a listing entry by ID + * @param id the ID of the listing to get + * @returns the listing as a JS object + */ +async function getListing(id: string): Promise { + await connectToDatabase(); + const listing = await ListingModel.findById(id).exec(); + return listing ? toListing(listing) : null; +} + +/** + * Add new listing entry + * @param newListing the listing data to add + * @returns the created listing as JS object + */ +async function addListing(newListing: ListingInput): Promise { + await connectToDatabase(); + const createdListing = await ListingModel.create(newListing); + return toListing(createdListing); +} + +/** + * Update listing entry by ID + * @param id the ID of the listing to update + * @param data the data passed in to partially or fully update the listing + * @returns the updated listing or null if not found + */ +async function updateListing( + id: string, + data: Partial +): Promise { + await connectToDatabase(); + const updatedListing = await ListingModel.findByIdAndUpdate(id, data, { + new: true, + runValidators: true, + }).exec(); + return updatedListing ? toListing(updatedListing) : null; +} + +/** + * Delete a listing entry by ID + * @param id the ID of the listing to delete + * @returns true if the listing was deleted, false otherwise + */ +// DON'T use this for tables that you don't actually need to potentially delete things from +// Could be used accidentally or misused maliciously to get rid of important data +async function deleteListing(id: string): Promise { + await connectToDatabase(); + const deleted = await ListingModel.findByIdAndDelete(id).exec(); + return Boolean(deleted); +} + +export { + getFilteredListings, + getListing, + addListing, + updateListing, + deleteListing, +}; From 2531c8016510df907ac798f540e8c4ef5f312ab3 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Wed, 18 Feb 2026 21:36:02 -0800 Subject: [PATCH 06/20] Refactored listing API methods to ensure consistency with service-wide standards --- app/api/listings/[id]/route.ts | 152 +++++++++++++++++++++++++++++---- app/api/listings/route.ts | 40 +++++---- services/listings/listings.ts | 12 +-- 3 files changed, 162 insertions(+), 42 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 6ff10e0..6f425c0 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -1,17 +1,26 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; import { z } from "zod"; -import Listing from "@/models/Listing"; -import mongoose from "mongoose"; +import { + deleteListing, + getListing, + updateListing, +} from "@/services/listings/listings"; /* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ +const objectIdSchema = z + .string() + .regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId"); const listingValidationSchema = z.object({ itemId: z.string().min(1), labId: z.string().min(1), quantityAvailable: z.number().min(1), + status: z.enum(["ACTIVE", "INACTIVE"]), }); -// helper method to verify connection +/** + * helper method to verify connection + */ async function connect() { try { await connectToDatabase(); @@ -23,18 +32,18 @@ async function connect() { } } -// GET: Return a single listing in the db -// input: req for specific listing with given id -// (ex: /listings/001) -// output: json response with the listing as a JS object if found -async function GET(request: Request) { +/** + * 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 connectionResponse = await connect(); if (connectionResponse) return connectionResponse; - const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - - if (!id || !mongoose.isValidObjectId(id)) { + const parsedId = objectIdSchema.safeParse(params.id); + if (!parsedId.success) { return NextResponse.json( { success: false, @@ -45,7 +54,7 @@ async function GET(request: Request) { } try { - const listing = await Listing.findById(id).lean(); // don't need mongo doc features + const listing = await getListing(parsedId.data); // don't need mongo doc features if (!listing) { return NextResponse.json( { success: false, message: "Listing not found." }, @@ -61,14 +70,127 @@ async function GET(request: Request) { } } -async function PUT(request: Request) { +/** + * 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 connectionResponse = await connect(); if (connectionResponse) return connectionResponse; + + 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 body = await request.json(); + + const validator = z.object({ + id: objectIdSchema, + update: listingValidationSchema.partial(), + }); + + const parsedRequest = validator.safeParse({ + id: parsedId.data, + update: body, + }); + if (!parsedRequest.success) { + return NextResponse.json( + { + success: false, + message: "Invalid request body.", + }, + { status: 400 } + ); + } + + try { + const updatedListing = await updateListing(parsedId.data, body); + 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 } + ); + } } -async function DELETE(request: Request) { +/** + * 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 connectionResponse = await connect(); if (connectionResponse) return connectionResponse; + + 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: 204 } + ); + } catch { + return NextResponse.json( + { + success: false, + message: "Error occurred while deleting listing.", + }, + { status: 500 } + ); + } } export { GET, PUT, DELETE }; diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 39eea91..7d2f907 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -1,23 +1,19 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; import { z } from "zod"; -import Listing from "@/models/Listing"; -import { - getFilteredListings, - getListing, - addListing, - updateListing, - deleteListing, -} from "@/services/listings/listings"; +import { getFilteredListings, addListing } from "@/services/listings/listings"; /* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ const listingValidationSchema = z.object({ itemId: z.string().min(1), labId: z.string().min(1), quantityAvailable: z.number().min(1), + status: z.enum(["ACTIVE", "INACTIVE"]), }); -// helper method to verify connection +/** + * helper method to verify connection + */ async function connect() { try { await connectToDatabase(); @@ -29,10 +25,12 @@ async function connect() { } } -// GET: Return a number of filtered listings stored in db -// input: req for an amount of certain listings -// (ex: /listings/?labId=3&page=2&limit=5) -// output: 10 possibly filtered listings from the db +/** + * Get filtered listings stored in db + * @param request the request + * ex req: GET /listings/?labId=3&page=2&limit=5 HTTP/1.1 + * @returns JSON response with the filtered listings as JS objects + */ async function GET(request: Request) { const connectionResponse = await connect(); if (connectionResponse) return connectionResponse; @@ -67,8 +65,11 @@ async function GET(request: Request) { } } -// POST: Create a new listing in DB -// input: post request with json data in body +/** + * Create a new listing to store in db + * @param request the request with JSON data in req body + * @returns JSON response with success message and req body echoed + */ async function POST(request: Request) { const connectionResponse = await connect(); if (connectionResponse) return connectionResponse; @@ -89,18 +90,15 @@ async function POST(request: Request) { } try { - const listing = await Listing.create({ - ...parsedBody.data, - // could possibly have {timestamps:true in schema to remove date stamp here} - createdAt: new Date(), - }); + const listingData = { ...parsedBody.data, createdAt: new Date() }; + const listing = await addListing(listingData); return NextResponse.json( { success: true, message: "Successfully created new listing.", data: listing, }, - { status: 201, headers: { Location: `/app/listings/${listing._id}` } } + { status: 201, headers: { Location: `/app/listings/${listing.id}` } } ); } catch (error: any) { if (error.code === 11000) { diff --git a/services/listings/listings.ts b/services/listings/listings.ts index a84c670..ff79770 100644 --- a/services/listings/listings.ts +++ b/services/listings/listings.ts @@ -13,14 +13,14 @@ interface FilterParams { } /** - * Get all listing entries + * Get all listing entries (likely unused since filtered & paginated more realistic) * @returns array of listings as JS objects */ -// async function getListings(): Promise { -// await connectToDatabase(); -// const listings = await ListingModel.find().exec(); -// return listings.map((listing) => toListing(listing)); -// } +async function getListings(): Promise { + await connectToDatabase(); + const listings = await ListingModel.find().exec(); + return listings.map((listing) => toListing(listing)); +} /** * Get filtered listing entries From 6c4024b78d0d6b47b85ca72e9966e0c7e86e5295 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Thu, 19 Feb 2026 20:13:19 -0800 Subject: [PATCH 07/20] Fixing db connections and incorrect variables in listing api routes --- app/api/listings/[id]/route.ts | 43 ++++++++++++++++++++-------------- app/api/listings/route.ts | 30 +++++++++++------------- services/listings/listings.ts | 8 +------ 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 6f425c0..83fb400 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -19,9 +19,12 @@ const listingValidationSchema = z.object({ }); /** - * helper method to verify connection + * 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 connect() { +async function GET(request: Request, { params }: { params: { id: string } }) { try { await connectToDatabase(); } catch { @@ -30,17 +33,6 @@ async function connect() { { status: 500 } ); } -} - -/** - * 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 connectionResponse = await connect(); - if (connectionResponse) return connectionResponse; const parsedId = objectIdSchema.safeParse(params.id); if (!parsedId.success) { @@ -76,8 +68,14 @@ async function GET(request: Request, { params }: { params: { id: string } }) { * @returns the updated listing as a JS object in a JSON response */ async function PUT(request: Request, { params }: { params: { id: string } }) { - const connectionResponse = await connect(); - if (connectionResponse) return connectionResponse; + 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) { @@ -112,7 +110,10 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { } try { - const updatedListing = await updateListing(parsedId.data, body); + const updatedListing = await updateListing( + parsedId.data, + parsedRequest.data.update + ); if (!updatedListing) { return NextResponse.json( { @@ -150,8 +151,14 @@ async function DELETE( request: Request, { params }: { params: { id: string } } ) { - const connectionResponse = await connect(); - if (connectionResponse) return connectionResponse; + 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) { diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 7d2f907..823cf96 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -12,28 +12,20 @@ const listingValidationSchema = z.object({ }); /** - * helper method to verify connection + * Get filtered listings stored in db + * @param request the request + * ex req: GET /listings/?labId=3&page=2&limit=5 HTTP/1.1 + * @returns JSON response with the filtered listings as JS objects */ -async function connect() { +async function GET(request: Request) { try { await connectToDatabase(); } catch { return NextResponse.json( - { success: false, message: "Error connecting to database" }, + { success: false, message: "Error connecting to database." }, { status: 500 } ); } -} - -/** - * Get filtered listings stored in db - * @param request the request - * ex req: GET /listings/?labId=3&page=2&limit=5 HTTP/1.1 - * @returns JSON response with the filtered listings as JS objects - */ -async function GET(request: Request) { - const connectionResponse = await connect(); - if (connectionResponse) return connectionResponse; const { searchParams } = new URL(request.url); const labId = searchParams.get("labId") || undefined; @@ -71,8 +63,14 @@ async function GET(request: Request) { * @returns JSON response with success message and req body echoed */ async function POST(request: Request) { - const connectionResponse = await connect(); - if (connectionResponse) return connectionResponse; + try { + await connectToDatabase(); + } catch { + return NextResponse.json( + { success: false, message: "Error connecting to database." }, + { status: 500 } + ); + } // assuming frontend sends req with content-type set to app/json // content type automatically set as app/json diff --git a/services/listings/listings.ts b/services/listings/listings.ts index ff79770..9f36e34 100644 --- a/services/listings/listings.ts +++ b/services/listings/listings.ts @@ -1,5 +1,4 @@ import type { HydratedDocument } from "mongoose"; -import { connectToDatabase } from "@/lib/mongoose"; import ListingModel, { Listing, ListingInput } from "@/models/Listing"; type ListingDocument = HydratedDocument; @@ -17,7 +16,6 @@ interface FilterParams { * @returns array of listings as JS objects */ async function getListings(): Promise { - await connectToDatabase(); const listings = await ListingModel.find().exec(); return listings.map((listing) => toListing(listing)); } @@ -40,7 +38,7 @@ async function getFilteredListings({ const validPage = isNaN(page) || page < 1 ? 1 : page; const validLimit = isNaN(limit) || limit < 1 ? 10 : Math.min(limit, MAX_LIMIT); - const skip = (page - 1) * limit; + const skip = (page - 1) * validLimit; const [listings, total] = await Promise.all([ ListingModel.find(query) @@ -68,7 +66,6 @@ async function getFilteredListings({ * @returns the listing as a JS object */ async function getListing(id: string): Promise { - await connectToDatabase(); const listing = await ListingModel.findById(id).exec(); return listing ? toListing(listing) : null; } @@ -79,7 +76,6 @@ async function getListing(id: string): Promise { * @returns the created listing as JS object */ async function addListing(newListing: ListingInput): Promise { - await connectToDatabase(); const createdListing = await ListingModel.create(newListing); return toListing(createdListing); } @@ -94,7 +90,6 @@ async function updateListing( id: string, data: Partial ): Promise { - await connectToDatabase(); const updatedListing = await ListingModel.findByIdAndUpdate(id, data, { new: true, runValidators: true, @@ -110,7 +105,6 @@ async function updateListing( // DON'T use this for tables that you don't actually need to potentially delete things from // Could be used accidentally or misused maliciously to get rid of important data async function deleteListing(id: string): Promise { - await connectToDatabase(); const deleted = await ListingModel.findByIdAndDelete(id).exec(); return Boolean(deleted); } From 91bf583832d51696e28f4e23fb023fd67c712e24 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Sat, 28 Feb 2026 20:43:13 -0800 Subject: [PATCH 08/20] Push commit to open PR for testing help --- coverage/clover.xml | 131 ++++ coverage/coverage-final.json | 4 + .../app/api/listings/[id]/index.html | 116 +++ .../app/api/listings/[id]/route.ts.html | 694 ++++++++++++++++++ .../lcov-report/app/api/listings/index.html | 116 +++ .../app/api/listings/route.ts.html | 433 +++++++++++ coverage/lcov-report/base.css | 224 ++++++ coverage/lcov-report/block-navigation.js | 87 +++ coverage/lcov-report/example.ts.html | 118 +++ coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes coverage/lcov-report/index.html | 146 ++++ coverage/lcov-report/mongoose.ts.html | 301 ++++++++ coverage/lcov-report/prettify.css | 1 + coverage/lcov-report/prettify.js | 2 + .../lcov-report/services/listings/index.html | 116 +++ .../services/listings/listings.ts.html | 439 +++++++++++ coverage/lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/lcov-report/sorter.js | 210 ++++++ coverage/lcov-report/utils.ts.html | 106 +++ coverage/lcov.info | 204 +++++ jest.config.ts | 62 +- lib/__tests__/listings.id.route.test.ts | 0 lib/__tests__/listings.route.test.ts | 120 +++ lib/__tests__/mongoose.test.ts | 16 +- lib/__tests__/services.listings.test.ts | 0 lib/mongoose.ts | 24 +- package-lock.json | 151 ++++ package.json | 1 + playwright-report/.last-run.json | 6 + tsconfig.json | 14 +- 30 files changed, 3787 insertions(+), 55 deletions(-) create mode 100644 coverage/clover.xml create mode 100644 coverage/coverage-final.json create mode 100644 coverage/lcov-report/app/api/listings/[id]/index.html create mode 100644 coverage/lcov-report/app/api/listings/[id]/route.ts.html create mode 100644 coverage/lcov-report/app/api/listings/index.html create mode 100644 coverage/lcov-report/app/api/listings/route.ts.html create mode 100644 coverage/lcov-report/base.css create mode 100644 coverage/lcov-report/block-navigation.js create mode 100644 coverage/lcov-report/example.ts.html create mode 100644 coverage/lcov-report/favicon.png create mode 100644 coverage/lcov-report/index.html create mode 100644 coverage/lcov-report/mongoose.ts.html create mode 100644 coverage/lcov-report/prettify.css create mode 100644 coverage/lcov-report/prettify.js create mode 100644 coverage/lcov-report/services/listings/index.html create mode 100644 coverage/lcov-report/services/listings/listings.ts.html create mode 100644 coverage/lcov-report/sort-arrow-sprite.png create mode 100644 coverage/lcov-report/sorter.js create mode 100644 coverage/lcov-report/utils.ts.html create mode 100644 coverage/lcov.info create mode 100644 lib/__tests__/listings.id.route.test.ts create mode 100644 lib/__tests__/listings.route.test.ts create mode 100644 lib/__tests__/services.listings.test.ts create mode 100644 playwright-report/.last-run.json diff --git a/coverage/clover.xml b/coverage/clover.xml new file mode 100644 index 0000000..30f45d5 --- /dev/null +++ b/coverage/clover.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json new file mode 100644 index 0000000..b4df2a4 --- /dev/null +++ b/coverage/coverage-final.json @@ -0,0 +1,4 @@ +{"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/route.ts": {"path":"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/route.ts","statementMap":{"0":{"start":{"line":116,"column":9},"end":{"line":116,"column":12}},"1":{"start":{"line":116,"column":14},"end":{"line":116,"column":18}},"2":{"start":{"line":1,"column":0},"end":{"line":1,"column":43}},"3":{"start":{"line":2,"column":0},"end":{"line":2,"column":51}},"4":{"start":{"line":3,"column":0},"end":{"line":3,"column":24}},"5":{"start":{"line":4,"column":0},"end":{"line":4,"column":79}},"6":{"start":{"line":7,"column":32},"end":{"line":12,"column":2}},"7":{"start":{"line":21,"column":2},"end":{"line":28,"column":3}},"8":{"start":{"line":22,"column":4},"end":{"line":22,"column":30}},"9":{"start":{"line":24,"column":4},"end":{"line":27,"column":6}},"10":{"start":{"line":30,"column":27},"end":{"line":30,"column":47}},"11":{"start":{"line":31,"column":16},"end":{"line":31,"column":54}},"12":{"start":{"line":32,"column":17},"end":{"line":32,"column":56}},"13":{"start":{"line":33,"column":15},"end":{"line":33,"column":56}},"14":{"start":{"line":34,"column":16},"end":{"line":34,"column":59}},"15":{"start":{"line":36,"column":2},"end":{"line":57,"column":3}},"16":{"start":{"line":37,"column":37},"end":{"line":42,"column":6}},"17":{"start":{"line":44,"column":4},"end":{"line":51,"column":6}},"18":{"start":{"line":53,"column":4},"end":{"line":56,"column":6}},"19":{"start":{"line":66,"column":2},"end":{"line":73,"column":3}},"20":{"start":{"line":67,"column":4},"end":{"line":67,"column":30}},"21":{"start":{"line":69,"column":4},"end":{"line":72,"column":6}},"22":{"start":{"line":77,"column":15},"end":{"line":77,"column":35}},"23":{"start":{"line":78,"column":21},"end":{"line":78,"column":60}},"24":{"start":{"line":80,"column":2},"end":{"line":88,"column":3}},"25":{"start":{"line":81,"column":4},"end":{"line":87,"column":6}},"26":{"start":{"line":90,"column":2},"end":{"line":113,"column":3}},"27":{"start":{"line":91,"column":21},"end":{"line":91,"column":69}},"28":{"start":{"line":92,"column":20},"end":{"line":92,"column":49}},"29":{"start":{"line":93,"column":4},"end":{"line":100,"column":6}},"30":{"start":{"line":102,"column":4},"end":{"line":108,"column":5}},"31":{"start":{"line":103,"column":6},"end":{"line":107,"column":8}},"32":{"start":{"line":109,"column":4},"end":{"line":112,"column":6}}},"fnMap":{"0":{"name":"GET","decl":{"start":{"line":20,"column":15},"end":{"line":20,"column":18}},"loc":{"start":{"line":20,"column":35},"end":{"line":58,"column":1}}},"1":{"name":"POST","decl":{"start":{"line":65,"column":15},"end":{"line":65,"column":19}},"loc":{"start":{"line":65,"column":36},"end":{"line":114,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":31,"column":16},"end":{"line":31,"column":54}},"type":"binary-expr","locations":[{"start":{"line":31,"column":16},"end":{"line":31,"column":41}},{"start":{"line":31,"column":45},"end":{"line":31,"column":54}}]},"1":{"loc":{"start":{"line":32,"column":17},"end":{"line":32,"column":56}},"type":"binary-expr","locations":[{"start":{"line":32,"column":17},"end":{"line":32,"column":43}},{"start":{"line":32,"column":47},"end":{"line":32,"column":56}}]},"2":{"loc":{"start":{"line":33,"column":24},"end":{"line":33,"column":55}},"type":"binary-expr","locations":[{"start":{"line":33,"column":24},"end":{"line":33,"column":48}},{"start":{"line":33,"column":52},"end":{"line":33,"column":55}}]},"3":{"loc":{"start":{"line":34,"column":25},"end":{"line":34,"column":58}},"type":"binary-expr","locations":[{"start":{"line":34,"column":25},"end":{"line":34,"column":50}},{"start":{"line":34,"column":54},"end":{"line":34,"column":58}}]},"4":{"loc":{"start":{"line":80,"column":2},"end":{"line":88,"column":3}},"type":"if","locations":[{"start":{"line":80,"column":2},"end":{"line":88,"column":3}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":102,"column":4},"end":{"line":108,"column":5}},"type":"if","locations":[{"start":{"line":102,"column":4},"end":{"line":108,"column":5}},{"start":{},"end":{}}]}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0},"f":{"0":0,"1":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0]}} +,"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/[id]/route.ts": {"path":"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/[id]/route.ts","statementMap":{"0":{"start":{"line":203,"column":9},"end":{"line":203,"column":12}},"1":{"start":{"line":203,"column":14},"end":{"line":203,"column":17}},"2":{"start":{"line":203,"column":19},"end":{"line":203,"column":25}},"3":{"start":{"line":1,"column":0},"end":{"line":1,"column":43}},"4":{"start":{"line":2,"column":0},"end":{"line":2,"column":51}},"5":{"start":{"line":3,"column":0},"end":{"line":3,"column":24}},"6":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"7":{"start":{"line":11,"column":23},"end":{"line":13,"column":57}},"8":{"start":{"line":14,"column":32},"end":{"line":19,"column":2}},"9":{"start":{"line":28,"column":2},"end":{"line":35,"column":3}},"10":{"start":{"line":29,"column":4},"end":{"line":29,"column":30}},"11":{"start":{"line":31,"column":4},"end":{"line":34,"column":6}},"12":{"start":{"line":37,"column":19},"end":{"line":37,"column":54}},"13":{"start":{"line":38,"column":2},"end":{"line":46,"column":3}},"14":{"start":{"line":39,"column":4},"end":{"line":45,"column":6}},"15":{"start":{"line":48,"column":2},"end":{"line":62,"column":3}},"16":{"start":{"line":49,"column":20},"end":{"line":49,"column":51}},"17":{"start":{"line":50,"column":4},"end":{"line":55,"column":5}},"18":{"start":{"line":51,"column":6},"end":{"line":54,"column":8}},"19":{"start":{"line":56,"column":4},"end":{"line":56,"column":80}},"20":{"start":{"line":58,"column":4},"end":{"line":61,"column":6}},"21":{"start":{"line":71,"column":2},"end":{"line":78,"column":3}},"22":{"start":{"line":72,"column":4},"end":{"line":72,"column":30}},"23":{"start":{"line":74,"column":4},"end":{"line":77,"column":6}},"24":{"start":{"line":80,"column":19},"end":{"line":80,"column":54}},"25":{"start":{"line":81,"column":2},"end":{"line":89,"column":3}},"26":{"start":{"line":82,"column":4},"end":{"line":88,"column":6}},"27":{"start":{"line":91,"column":15},"end":{"line":91,"column":35}},"28":{"start":{"line":93,"column":20},"end":{"line":96,"column":4}},"29":{"start":{"line":98,"column":24},"end":{"line":101,"column":4}},"30":{"start":{"line":102,"column":2},"end":{"line":110,"column":3}},"31":{"start":{"line":103,"column":4},"end":{"line":109,"column":6}},"32":{"start":{"line":112,"column":2},"end":{"line":142,"column":3}},"33":{"start":{"line":113,"column":27},"end":{"line":115,"column":null}},"34":{"start":{"line":117,"column":4},"end":{"line":125,"column":5}},"35":{"start":{"line":118,"column":6},"end":{"line":124,"column":8}},"36":{"start":{"line":126,"column":4},"end":{"line":133,"column":6}},"37":{"start":{"line":135,"column":4},"end":{"line":141,"column":6}},"38":{"start":{"line":154,"column":2},"end":{"line":161,"column":3}},"39":{"start":{"line":155,"column":4},"end":{"line":155,"column":30}},"40":{"start":{"line":157,"column":4},"end":{"line":160,"column":6}},"41":{"start":{"line":163,"column":19},"end":{"line":163,"column":54}},"42":{"start":{"line":164,"column":2},"end":{"line":172,"column":3}},"43":{"start":{"line":165,"column":4},"end":{"line":171,"column":6}},"44":{"start":{"line":174,"column":2},"end":{"line":200,"column":3}},"45":{"start":{"line":175,"column":20},"end":{"line":175,"column":54}},"46":{"start":{"line":176,"column":4},"end":{"line":184,"column":5}},"47":{"start":{"line":177,"column":6},"end":{"line":183,"column":8}},"48":{"start":{"line":185,"column":4},"end":{"line":191,"column":6}},"49":{"start":{"line":193,"column":4},"end":{"line":199,"column":6}}},"fnMap":{"0":{"name":"GET","decl":{"start":{"line":27,"column":15},"end":{"line":27,"column":18}},"loc":{"start":{"line":27,"column":75},"end":{"line":63,"column":1}}},"1":{"name":"PUT","decl":{"start":{"line":70,"column":15},"end":{"line":70,"column":18}},"loc":{"start":{"line":70,"column":75},"end":{"line":143,"column":1}}},"2":{"name":"DELETE","decl":{"start":{"line":150,"column":15},"end":{"line":150,"column":21}},"loc":{"start":{"line":152,"column":40},"end":{"line":201,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":38,"column":2},"end":{"line":46,"column":3}},"type":"if","locations":[{"start":{"line":38,"column":2},"end":{"line":46,"column":3}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":50,"column":4},"end":{"line":55,"column":5}},"type":"if","locations":[{"start":{"line":50,"column":4},"end":{"line":55,"column":5}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":81,"column":2},"end":{"line":89,"column":3}},"type":"if","locations":[{"start":{"line":81,"column":2},"end":{"line":89,"column":3}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":102,"column":2},"end":{"line":110,"column":3}},"type":"if","locations":[{"start":{"line":102,"column":2},"end":{"line":110,"column":3}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":117,"column":4},"end":{"line":125,"column":5}},"type":"if","locations":[{"start":{"line":117,"column":4},"end":{"line":125,"column":5}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":164,"column":2},"end":{"line":172,"column":3}},"type":"if","locations":[{"start":{"line":164,"column":2},"end":{"line":172,"column":3}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":176,"column":4},"end":{"line":184,"column":5}},"type":"if","locations":[{"start":{"line":176,"column":4},"end":{"line":184,"column":5}},{"start":{},"end":{}}]}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0},"f":{"0":0,"1":0,"2":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0]}} +,"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/services/listings/listings.ts": {"path":"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/services/listings/listings.ts","statementMap":{"0":{"start":{"line":113,"column":2},"end":{"line":113,"column":21}},"1":{"start":{"line":114,"column":2},"end":{"line":114,"column":12}},"2":{"start":{"line":115,"column":2},"end":{"line":115,"column":12}},"3":{"start":{"line":116,"column":2},"end":{"line":116,"column":15}},"4":{"start":{"line":117,"column":2},"end":{"line":117,"column":15}},"5":{"start":{"line":2,"column":0},"end":{"line":2,"column":71}},"6":{"start":{"line":5,"column":18},"end":{"line":5,"column":76}},"7":{"start":{"line":5,"column":53},"end":{"line":5,"column":76}},"8":{"start":{"line":19,"column":19},"end":{"line":19,"column":51}},"9":{"start":{"line":20,"column":2},"end":{"line":20,"column":55}},"10":{"start":{"line":20,"column":35},"end":{"line":20,"column":53}},"11":{"start":{"line":33,"column":21},"end":{"line":33,"column":23}},"12":{"start":{"line":34,"column":2},"end":{"line":34,"column":33}},"13":{"start":{"line":34,"column":13},"end":{"line":34,"column":33}},"14":{"start":{"line":35,"column":2},"end":{"line":35,"column":36}},"15":{"start":{"line":35,"column":14},"end":{"line":35,"column":36}},"16":{"start":{"line":37,"column":20},"end":{"line":37,"column":22}},"17":{"start":{"line":38,"column":20},"end":{"line":38,"column":54}},"18":{"start":{"line":40,"column":4},"end":{"line":40,"column":63}},"19":{"start":{"line":41,"column":15},"end":{"line":41,"column":38}},"20":{"start":{"line":43,"column":28},"end":{"line":50,"column":4}},"21":{"start":{"line":52,"column":2},"end":{"line":60,"column":4}},"22":{"start":{"line":53,"column":40},"end":{"line":53,"column":58}},"23":{"start":{"line":69,"column":18},"end":{"line":69,"column":56}},"24":{"start":{"line":70,"column":2},"end":{"line":70,"column":45}},"25":{"start":{"line":79,"column":25},"end":{"line":79,"column":62}},"26":{"start":{"line":80,"column":2},"end":{"line":80,"column":35}},"27":{"start":{"line":93,"column":25},"end":{"line":96,"column":11}},"28":{"start":{"line":97,"column":2},"end":{"line":97,"column":59}},"29":{"start":{"line":108,"column":18},"end":{"line":108,"column":65}},"30":{"start":{"line":109,"column":2},"end":{"line":109,"column":26}}},"fnMap":{"0":{"name":"(anonymous_1)","decl":{"start":{"line":5,"column":18},"end":{"line":5,"column":19}},"loc":{"start":{"line":5,"column":53},"end":{"line":5,"column":76}}},"1":{"name":"getListings","decl":{"start":{"line":18,"column":15},"end":{"line":18,"column":26}},"loc":{"start":{"line":18,"column":26},"end":{"line":21,"column":1}}},"2":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":22},"end":{"line":20,"column":23}},"loc":{"start":{"line":20,"column":35},"end":{"line":20,"column":53}}},"3":{"name":"getFilteredListings","decl":{"start":{"line":27,"column":15},"end":{"line":27,"column":34}},"loc":{"start":{"line":32,"column":15},"end":{"line":61,"column":1}}},"4":{"name":"(anonymous_5)","decl":{"start":{"line":53,"column":27},"end":{"line":53,"column":28}},"loc":{"start":{"line":53,"column":40},"end":{"line":53,"column":58}}},"5":{"name":"getListing","decl":{"start":{"line":68,"column":15},"end":{"line":68,"column":25}},"loc":{"start":{"line":68,"column":36},"end":{"line":71,"column":1}}},"6":{"name":"addListing","decl":{"start":{"line":78,"column":15},"end":{"line":78,"column":25}},"loc":{"start":{"line":78,"column":50},"end":{"line":81,"column":1}}},"7":{"name":"updateListing","decl":{"start":{"line":89,"column":15},"end":{"line":89,"column":28}},"loc":{"start":{"line":91,"column":29},"end":{"line":98,"column":1}}},"8":{"name":"deleteListing","decl":{"start":{"line":107,"column":15},"end":{"line":107,"column":28}},"loc":{"start":{"line":107,"column":39},"end":{"line":110,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":34,"column":2},"end":{"line":34,"column":33}},"type":"if","locations":[{"start":{"line":34,"column":2},"end":{"line":34,"column":33}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":35,"column":2},"end":{"line":35,"column":36}},"type":"if","locations":[{"start":{"line":35,"column":2},"end":{"line":35,"column":36}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":38,"column":20},"end":{"line":38,"column":54}},"type":"cond-expr","locations":[{"start":{"line":38,"column":46},"end":{"line":38,"column":47}},{"start":{"line":38,"column":50},"end":{"line":38,"column":54}}]},"3":{"loc":{"start":{"line":38,"column":20},"end":{"line":38,"column":43}},"type":"binary-expr","locations":[{"start":{"line":38,"column":20},"end":{"line":38,"column":31}},{"start":{"line":38,"column":35},"end":{"line":38,"column":43}}]},"4":{"loc":{"start":{"line":40,"column":4},"end":{"line":40,"column":63}},"type":"cond-expr","locations":[{"start":{"line":40,"column":32},"end":{"line":40,"column":34}},{"start":{"line":40,"column":37},"end":{"line":40,"column":63}}]},"5":{"loc":{"start":{"line":40,"column":4},"end":{"line":40,"column":29}},"type":"binary-expr","locations":[{"start":{"line":40,"column":4},"end":{"line":40,"column":16}},{"start":{"line":40,"column":20},"end":{"line":40,"column":29}}]},"6":{"loc":{"start":{"line":70,"column":9},"end":{"line":70,"column":44}},"type":"cond-expr","locations":[{"start":{"line":70,"column":19},"end":{"line":70,"column":37}},{"start":{"line":70,"column":40},"end":{"line":70,"column":44}}]},"7":{"loc":{"start":{"line":97,"column":9},"end":{"line":97,"column":58}},"type":"cond-expr","locations":[{"start":{"line":97,"column":26},"end":{"line":97,"column":51}},{"start":{"line":97,"column":54},"end":{"line":97,"column":58}}]}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0]}} +} diff --git a/coverage/lcov-report/app/api/listings/[id]/index.html b/coverage/lcov-report/app/api/listings/[id]/index.html new file mode 100644 index 0000000..9de2934 --- /dev/null +++ b/coverage/lcov-report/app/api/listings/[id]/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for app/api/listings/[id] + + + + + + + + + +
+
+

All files app/api/listings/[id]

+
+ +
+ 0% + Statements + 0/50 +
+ + +
+ 0% + Branches + 0/14 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
route.ts +
+
0%0/500%0/140%0/30%0/48
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/app/api/listings/[id]/route.ts.html b/coverage/lcov-report/app/api/listings/[id]/route.ts.html new file mode 100644 index 0000000..34e5871 --- /dev/null +++ b/coverage/lcov-report/app/api/listings/[id]/route.ts.html @@ -0,0 +1,694 @@ + + + + + + Code coverage report for app/api/listings/[id]/route.ts + + + + + + + + + +
+
+

All files / app/api/listings/[id] route.ts

+
+ +
+ 0% + Statements + 0/50 +
+ + +
+ 0% + Branches + 0/14 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 0% + Lines + 0/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { NextResponse } from "next/server";
+import { connectToDatabase } from "@/lib/mongoose";
+import { z } from "zod";
+import {
+  deleteListing,
+  getListing,
+  updateListing,
+} from "@/services/listings/listings";
+ 
+/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */
+const objectIdSchema = z
+  .string()
+  .regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId");
+const listingValidationSchema = z.object({
+  itemId: z.string().min(1),
+  labId: z.string().min(1),
+  quantityAvailable: z.number().min(1),
+  status: z.enum(["ACTIVE", "INACTIVE"]),
+});
+ 
+/**
+ * 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 } }) {
+  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 } }) {
+  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 body = await request.json();
+ 
+  const validator = z.object({
+    id: objectIdSchema,
+    update: listingValidationSchema.partial(),
+  });
+ 
+  const parsedRequest = validator.safeParse({
+    id: parsedId.data,
+    update: body,
+  });
+  if (!parsedRequest.success) {
+    return NextResponse.json(
+      {
+        success: false,
+        message: "Invalid request body.",
+      },
+      { status: 400 }
+    );
+  }
+ 
+  try {
+    const updatedListing = await updateListing(
+      parsedId.data,
+      parsedRequest.data.update
+    );
+    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 } }
+) {
+  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: 204 }
+    );
+  } catch {
+    return NextResponse.json(
+      {
+        success: false,
+        message: "Error occurred while deleting listing.",
+      },
+      { status: 500 }
+    );
+  }
+}
+ 
+export { GET, PUT, DELETE };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/app/api/listings/index.html b/coverage/lcov-report/app/api/listings/index.html new file mode 100644 index 0000000..9c23506 --- /dev/null +++ b/coverage/lcov-report/app/api/listings/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for app/api/listings + + + + + + + + + +
+
+

All files app/api/listings

+
+ +
+ 0% + Statements + 0/33 +
+ + +
+ 0% + Branches + 0/12 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/32 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
route.ts +
+
0%0/330%0/120%0/20%0/32
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/app/api/listings/route.ts.html b/coverage/lcov-report/app/api/listings/route.ts.html new file mode 100644 index 0000000..9454931 --- /dev/null +++ b/coverage/lcov-report/app/api/listings/route.ts.html @@ -0,0 +1,433 @@ + + + + + + Code coverage report for app/api/listings/route.ts + + + + + + + + + +
+
+

All files / app/api/listings route.ts

+
+ +
+ 0% + Statements + 0/33 +
+ + +
+ 0% + Branches + 0/12 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/32 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { NextResponse } from "next/server";
+import { connectToDatabase } from "@/lib/mongoose";
+import { z } from "zod";
+import { getFilteredListings, addListing } from "@/services/listings/listings";
+ 
+/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */
+const listingValidationSchema = z.object({
+  itemId: z.string().min(1),
+  labId: z.string().min(1),
+  quantityAvailable: z.number().min(1),
+  status: z.enum(["ACTIVE", "INACTIVE"]),
+});
+ 
+/**
+ * Get filtered listings stored in db
+ * @param request the request
+ * ex req: GET /listings/?labId=3&page=2&limit=5 HTTP/1.1
+ * @returns JSON response with the filtered listings as JS objects
+ */
+async function GET(request: Request) {
+  try {
+    await connectToDatabase();
+  } catch {
+    return NextResponse.json(
+      { success: false, message: "Error connecting to database." },
+      { status: 500 }
+    );
+  }
+ 
+  const { searchParams } = new URL(request.url);
+  const labId = searchParams.get("labId") || undefined;
+  const itemId = searchParams.get("itemId") || undefined;
+  const page = parseInt(searchParams.get("page") || "1");
+  const limit = parseInt(searchParams.get("limit") || "10");
+ 
+  try {
+    const { listings, pagination } = await getFilteredListings({
+      labId,
+      itemId,
+      page,
+      limit,
+    });
+ 
+    return NextResponse.json(
+      {
+        success: true,
+        data: listings,
+        pagination,
+      },
+      { status: 200 }
+    );
+  } catch {
+    return NextResponse.json(
+      { success: false, message: "Error occurred while retrieving listings." },
+      { status: 500 }
+    );
+  }
+}
+ 
+/**
+ * Create a new listing to store in db
+ * @param request the request with JSON data in req body
+ * @returns JSON response with success message and req body echoed
+ */
+async function POST(request: Request) {
+  try {
+    await connectToDatabase();
+  } catch {
+    return NextResponse.json(
+      { success: false, message: "Error connecting to database." },
+      { status: 500 }
+    );
+  }
+ 
+  // assuming frontend sends req with content-type set to app/json
+  // content type automatically set as app/json
+  const body = await request.json();
+  const parsedBody = listingValidationSchema.safeParse(body);
+ 
+  if (!parsedBody.success) {
+    return NextResponse.json(
+      {
+        success: false,
+        message: "Invalid request body.",
+      },
+      { status: 400 }
+    );
+  }
+ 
+  try {
+    const listingData = { ...parsedBody.data, createdAt: new Date() };
+    const listing = await addListing(listingData);
+    return NextResponse.json(
+      {
+        success: true,
+        message: "Successfully created new listing.",
+        data: listing,
+      },
+      { status: 201, headers: { Location: `/app/listings/${listing.id}` } }
+    );
+  } catch (error: any) {
+    if (error.code === 11000) {
+      return NextResponse.json(
+        // don't send mongo's error - exposes design info
+        { success: false, message: "This listing already exists." },
+        { status: 409 }
+      );
+    }
+    return NextResponse.json(
+      { success: false, message: "Error occurred while creating new listing." },
+      { status: 500 }
+    );
+  }
+}
+ 
+export { GET, POST };
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/example.ts.html b/coverage/lcov-report/example.ts.html new file mode 100644 index 0000000..469a6a0 --- /dev/null +++ b/coverage/lcov-report/example.ts.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for example.ts + + + + + + + + + +
+
+

All files example.ts

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +  +  +  +  +  +  + 
// Example
+ 
+/**
+ * A utility function that generates a greeting message.
+ *
+ * @param name - The name to greet.
+ * @returns A greeting message.
+ */
+export function greetUser(name: string): string {
+    return `Hello, ${name}! Welcome to our site.`;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 0% + Statements + 0/114 +
+ + +
+ 0% + Branches + 0/42 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 0% + Lines + 0/107 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
app/api/listings +
+
0%0/330%0/120%0/20%0/32
app/api/listings/[id] +
+
0%0/500%0/140%0/30%0/48
services/listings +
+
0%0/310%0/160%0/90%0/27
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/mongoose.ts.html b/coverage/lcov-report/mongoose.ts.html new file mode 100644 index 0000000..a1ecdd4 --- /dev/null +++ b/coverage/lcov-report/mongoose.ts.html @@ -0,0 +1,301 @@ + + + + + + Code coverage report for mongoose.ts + + + + + + + + + +
+
+

All files mongoose.ts

+
+ +
+ 83.33% + Statements + 20/24 +
+ + +
+ 75% + Branches + 9/12 +
+ + +
+ 66.66% + Functions + 2/3 +
+ + +
+ 83.33% + Lines + 20/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +731x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +1x +  +  +  +1x +  +1x +16x +  +16x +  +  +  +  +16x +2x +  +  +14x +5x +  +  +  +  +14x +14x +  +  +  +  +  +1x +5x +5x +5x +5x +5x +5x +  +  +  +  +  +  +  +  +  +  + 
import mongoose from "mongoose";
+ 
+// const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null
+ 
+// if (!MONGODB_URI) {
+//   throw new Error(
+//     "Please define the DATABASE_URL environment variable inside .env"
+//   );
+// }
+// move inside the connectDB function so the test script can access
+ 
+type MongooseCache = {
+  conn: typeof mongoose | null;
+  promise: Promise<typeof mongoose> | null;
+};
+ 
+declare global {
+  // eslint-disable-next-line no-var
+  var mongoose: MongooseCache | undefined;
+}
+ 
+const globalForMongoose = globalThis as typeof globalThis & {
+  mongoose?: MongooseCache;
+};
+const cached: MongooseCache = globalForMongoose.mongoose ?? {
+  conn: null,
+  promise: null,
+};
+globalForMongoose.mongoose = cached;
+ 
+export async function connectToDatabase() {
+  const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null
+ 
+  Iif (!MONGODB_URI) {
+    throw new Error(
+      "Please define the DATABASE_URL environment variable inside .env"
+    );
+  }
+  if (cached.conn) {
+    return cached.conn;
+  }
+ 
+  if (!cached.promise) {
+    cached.promise = mongoose.connect(MONGODB_URI, {
+      bufferCommands: false,
+    });
+  }
+ 
+  cached.conn = await cached.promise;
+  return cached.conn;
+}
+ 
+/**
+ * Disconnect from MongoDB for testing
+ */
+export async function disconnectDatabase() {
+  Eif (cached.conn) {
+    try {
+      await cached.conn.disconnect();
+      cached.conn = null;
+      cached.promise = null;
+      console.log("DB disconnected");
+    } catch (error) {
+      console.error("Error disconnecting from database", error);
+      throw error;
+    }
+  }
+}
+ 
+function test() {
+  console.log("not tested"); // making sure coverage can see which aren't tested
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/services/listings/index.html b/coverage/lcov-report/services/listings/index.html new file mode 100644 index 0000000..6c9eb88 --- /dev/null +++ b/coverage/lcov-report/services/listings/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for services/listings + + + + + + + + + +
+
+

All files services/listings

+
+ +
+ 0% + Statements + 0/31 +
+ + +
+ 0% + Branches + 0/16 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
listings.ts +
+
0%0/310%0/160%0/90%0/27
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/services/listings/listings.ts.html b/coverage/lcov-report/services/listings/listings.ts.html new file mode 100644 index 0000000..5b05e85 --- /dev/null +++ b/coverage/lcov-report/services/listings/listings.ts.html @@ -0,0 +1,439 @@ + + + + + + Code coverage report for services/listings/listings.ts + + + + + + + + + +
+
+

All files / services/listings listings.ts

+
+ +
+ 0% + Statements + 0/31 +
+ + +
+ 0% + Branches + 0/16 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import type { HydratedDocument } from "mongoose";
+import ListingModel, { Listing, ListingInput } from "@/models/Listing";
+ 
+type ListingDocument = HydratedDocument<ListingInput>;
+const toListing = (doc: ListingDocument): Listing => doc.toObject<Listing>();
+ 
+interface FilterParams {
+  labId?: string;
+  itemId?: string;
+  page: number;
+  limit: number;
+}
+ 
+/**
+ * Get all listing entries (likely unused since filtered & paginated more realistic)
+ * @returns array of listings as JS objects
+ */
+async function getListings(): Promise<Listing[]> {
+  const listings = await ListingModel.find().exec();
+  return listings.map((listing) => toListing(listing));
+}
+ 
+/**
+ * Get filtered listing entries
+ * @returns array of listings as JS objects
+ */
+async function getFilteredListings({
+  labId,
+  itemId,
+  page,
+  limit,
+}: FilterParams) {
+  const query: any = {};
+  if (labId) query.labId = labId;
+  if (itemId) query.itemId = itemId;
+ 
+  const MAX_LIMIT = 20; // inquire about this in the future
+  const validPage = isNaN(page) || page < 1 ? 1 : page;
+  const validLimit =
+    isNaN(limit) || limit < 1 ? 10 : Math.min(limit, MAX_LIMIT);
+  const skip = (page - 1) * validLimit;
+ 
+  const [listings, total] = await Promise.all([
+    ListingModel.find(query)
+      .sort({ createdAt: -1 }) // Sort from newest to oldest
+      .skip(skip)
+      .limit(validLimit)
+      .exec(), // toListing handles doc to JS object, so lean unncessary
+    ListingModel.countDocuments(query),
+  ]);
+ 
+  return {
+    listings: listings.map((listing) => toListing(listing)),
+    pagination: {
+      page: validPage,
+      limit: validLimit,
+      total,
+      totalPages: Math.ceil(total / validLimit),
+    },
+  };
+}
+ 
+/**
+ * Get a listing entry by ID
+ * @param id the ID of the listing to get
+ * @returns the listing as a JS object
+ */
+async function getListing(id: string): Promise<Listing | null> {
+  const listing = await ListingModel.findById(id).exec();
+  return listing ? toListing(listing) : null;
+}
+ 
+/**
+ * Add new listing entry
+ * @param newListing the listing data to add
+ * @returns the created listing as JS object
+ */
+async function addListing(newListing: ListingInput): Promise<Listing> {
+  const createdListing = await ListingModel.create(newListing);
+  return toListing(createdListing);
+}
+ 
+/**
+ * Update listing entry by ID
+ * @param id the ID of the listing to update
+ * @param data the data passed in to partially or fully update the listing
+ * @returns the updated listing or null if not found
+ */
+async function updateListing(
+  id: string,
+  data: Partial<ListingInput>
+): Promise<Listing | null> {
+  const updatedListing = await ListingModel.findByIdAndUpdate(id, data, {
+    new: true,
+    runValidators: true,
+  }).exec();
+  return updatedListing ? toListing(updatedListing) : null;
+}
+ 
+/**
+ * Delete a listing entry by ID
+ * @param id the ID of the listing to delete
+ * @returns true if the listing was deleted, false otherwise
+ */
+// DON'T use this for tables that you don't actually need to potentially delete things from
+// Could be used accidentally or misused maliciously to get rid of important data
+async function deleteListing(id: string): Promise<boolean> {
+  const deleted = await ListingModel.findByIdAndDelete(id).exec();
+  return Boolean(deleted);
+}
+ 
+export {
+  getFilteredListings,
+  getListing,
+  addListing,
+  updateListing,
+  deleteListing,
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/utils.ts.html b/coverage/lcov-report/utils.ts.html new file mode 100644 index 0000000..5b4571a --- /dev/null +++ b/coverage/lcov-report/utils.ts.html @@ -0,0 +1,106 @@ + + + + + + Code coverage report for utils.ts + + + + + + + + + +
+
+

All files utils.ts

+
+ +
+ 0% + Statements + 0/4 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8  +  +  +  +  +  +  + 
import { clsx } from "clsx";
+import type { ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+ 
+export function cn(...inputs: ClassValue[]) {
+    return twMerge(clsx(inputs));
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..50418b9 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,204 @@ +TN: +SF:app/api/listings/route.ts +FN:20,GET +FN:65,POST +FNF:2 +FNH:0 +FNDA:0,GET +FNDA:0,POST +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:7,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:44,0 +DA:53,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:81,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:102,0 +DA:103,0 +DA:109,0 +DA:116,0 +LF:32 +LH:0 +BRDA:31,0,0,0 +BRDA:31,0,1,0 +BRDA:32,1,0,0 +BRDA:32,1,1,0 +BRDA:33,2,0,0 +BRDA:33,2,1,0 +BRDA:34,3,0,0 +BRDA:34,3,1,0 +BRDA:80,4,0,0 +BRDA:80,4,1,0 +BRDA:102,5,0,0 +BRDA:102,5,1,0 +BRF:12 +BRH:0 +end_of_record +TN: +SF:app/api/listings/[id]/route.ts +FN:27,GET +FN:70,PUT +FN:150,DELETE +FNF:3 +FNH:0 +FNDA:0,GET +FNDA:0,PUT +FNDA:0,DELETE +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +DA:11,0 +DA:14,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:56,0 +DA:58,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:91,0 +DA:93,0 +DA:98,0 +DA:102,0 +DA:103,0 +DA:112,0 +DA:113,0 +DA:117,0 +DA:118,0 +DA:126,0 +DA:135,0 +DA:154,0 +DA:155,0 +DA:157,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:185,0 +DA:193,0 +DA:203,0 +LF:48 +LH:0 +BRDA:38,0,0,0 +BRDA:38,0,1,0 +BRDA:50,1,0,0 +BRDA:50,1,1,0 +BRDA:81,2,0,0 +BRDA:81,2,1,0 +BRDA:102,3,0,0 +BRDA:102,3,1,0 +BRDA:117,4,0,0 +BRDA:117,4,1,0 +BRDA:164,5,0,0 +BRDA:164,5,1,0 +BRDA:176,6,0,0 +BRDA:176,6,1,0 +BRF:14 +BRH:0 +end_of_record +TN: +SF:services/listings/listings.ts +FN:5,(anonymous_1) +FN:18,getListings +FN:20,(anonymous_3) +FN:27,getFilteredListings +FN:53,(anonymous_5) +FN:68,getListing +FN:78,addListing +FN:89,updateListing +FN:107,deleteListing +FNF:9 +FNH:0 +FNDA:0,(anonymous_1) +FNDA:0,getListings +FNDA:0,(anonymous_3) +FNDA:0,getFilteredListings +FNDA:0,(anonymous_5) +FNDA:0,getListing +FNDA:0,addListing +FNDA:0,updateListing +FNDA:0,deleteListing +DA:2,0 +DA:5,0 +DA:19,0 +DA:20,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:52,0 +DA:53,0 +DA:69,0 +DA:70,0 +DA:79,0 +DA:80,0 +DA:93,0 +DA:97,0 +DA:108,0 +DA:109,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +LF:27 +LH:0 +BRDA:34,0,0,0 +BRDA:34,0,1,0 +BRDA:35,1,0,0 +BRDA:35,1,1,0 +BRDA:38,2,0,0 +BRDA:38,2,1,0 +BRDA:38,3,0,0 +BRDA:38,3,1,0 +BRDA:40,4,0,0 +BRDA:40,4,1,0 +BRDA:40,5,0,0 +BRDA:40,5,1,0 +BRDA:70,6,0,0 +BRDA:70,6,1,0 +BRDA:97,7,0,0 +BRDA:97,7,1,0 +BRF:16 +BRH:0 +end_of_record diff --git a/jest.config.ts b/jest.config.ts index 6a2f4c6..6cf798c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,53 +1,55 @@ -import type { Config } from 'jest'; -import path from 'path'; +import type { Config } from "jest"; +import path from "path"; const config: Config = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: [''], + preset: "ts-jest", + testEnvironment: "node", + roots: [""], // Test file patterns testMatch: [ - '**/__tests__/**/*.test.ts', - '**/__tests__/**/*.spec.ts', - '**/.test.ts', - '**/.spec.ts', + "**/__tests__/**/*.test.ts", + "**/__tests__/**/*.spec.ts", + "**/.test.ts", + "**/.spec.ts", ], // Module path mapping (for @/ imports) moduleNameMapper: { - '^@/(.*)$': '/$1', + "^@/(.*)$": "/$1", }, // Setup files - setupFilesAfterEnv: ['/jest.setup.ts'], + setupFilesAfterEnv: ["/jest.setup.ts"], // Coverage settings collectCoverageFrom: [ - 'app/*/.ts', - 'app/*/.tsx', - 'services/*/.ts', - 'models/*/.ts', - 'lib/*/.ts', - '!*/.d.ts', - '!*/node_modules/*', - '!*/.next/*', - '!*/coverage/*', + "app/*/.ts", + "app/*/.tsx", + "app/api/listings/route.ts", + "app/api/listings/[id]/route.ts", + "services/*/.ts", + "services/listings/listings.ts", + "models/*/.ts", + // "lib/**/*.ts", // Include all TypeScript files in the lib directory + // "!lib/**/*.d.ts", // Exclude type declaration files + // "!lib/__tests__/**/*.ts", // Exclude test files + "lib/*/.ts", + "!*/.d.ts", + "!*/node_modules/*", + "!*/.next/*", + "!*/coverage/*", ], - coveragePathIgnorePatterns: [ - '/node_modules/', - '/.next/', - '/coverage/', - ], + coveragePathIgnorePatterns: ["/node_modules/", "/.next/", "/coverage/"], // Transform files transform: { - '^.+\\.tsx?$': [ - 'ts-jest', + "^.+\\.tsx?$": [ + "ts-jest", { tsconfig: { - jsx: 'react', + jsx: "react", esModuleInterop: true, }, }, @@ -55,7 +57,7 @@ const config: Config = { }, // Module file extensions - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], // Test timeout (important for DB tests) testTimeout: 10000, @@ -64,4 +66,4 @@ const config: Config = { verbose: true, }; -export default config; \ No newline at end of file +export default config; diff --git a/lib/__tests__/listings.id.route.test.ts b/lib/__tests__/listings.id.route.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/__tests__/listings.route.test.ts b/lib/__tests__/listings.route.test.ts new file mode 100644 index 0000000..edbb283 --- /dev/null +++ b/lib/__tests__/listings.route.test.ts @@ -0,0 +1,120 @@ +import { GET, POST } from "@/app/api/listings/route"; +import { getFilteredListings } from "@/services/listings/listings"; +import { mock } from "node:test"; + +jest.mock("@/services/listings/listings", () => ({ + getFilteredListings: jest.fn(), +})); + +describe("GET Request Filter Tests", () => { + // make sure tests are independent + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("successfully returns all listings with status 200", async () => { + // arrange + const mockListings = [ + { + id: "123", + itemId: "item1", + labId: "3", + quantityAvailable: 10, + status: "ACTIVE", + createdAt: new Date(), + }, + { + id: "456", + itemId: "item2", + labId: "3", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: new Date(), + }, + ]; + const mockPagination = { + page: 2, + limit: 5, + total: 5, + totalPages: 1, + }; + const mockResult = { + success: true, + data: mockListings, + pagination: mockPagination, + }; + + (getFilteredListings as jest.Mock).mockResolvedValue({ + listings: mockListings, + pagination: mockPagination, + }); + + const mockRequest = new Request("/listings?labId=3&page=2&limit=5", { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + // act + const response = await GET(mockRequest); + const responseBody = await response.json(); + + // assert + expect(responseBody).toHaveProperty("success", true); + expect(responseBody).toHaveProperty("data"); + expect(responseBody).toHaveProperty("pagination"); + expect(responseBody.data).toEqual(mockListings); + expect(responseBody.pagination.page).toEqual(2); + expect(responseBody.pagination.limit).toEqual(5); + expect(responseBody.pagination.total).toEqual(5); + expect(responseBody.pagination.totalPages).toEqual(1); + expect(responseBody).toEqual(mockResult); + + // Verify getFilteredListings was called correctly + expect(getFilteredListings).toHaveBeenCalledWith({ + labId: "3", + itemId: undefined, + page: 2, + limit: 5, + }); + }); +}); + +describe("POST Request Tests", () => { + // make sure tests are independent + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("creates a new listing successfully", async () => { + const listingData = {}; + const response = {}; + }); + + test("handles API error during incomplete listing creation", async () => { + // arrange + const mockResult = { + success: false, + message: "Invalid request body.", + }; + const mockPostData = JSON.stringify({ labId: 1 }); // data incomplete + const mockRequest = new Request("/listings", { + method: "POST", + body: mockPostData, + headers: { "Content-Type": "application/json" }, + }); + + // act + const response = await POST(mockRequest); + const responseBody = await response.json(); + + // assert + expect(response.status).toBe(400); + expect(responseBody).toEqual(mockResult); + }); +}); diff --git a/lib/__tests__/mongoose.test.ts b/lib/__tests__/mongoose.test.ts index a05922f..d6b2ed5 100644 --- a/lib/__tests__/mongoose.test.ts +++ b/lib/__tests__/mongoose.test.ts @@ -1,7 +1,7 @@ -import { connectToDatabase, disconnectDatabase } from '@/lib/mongoose'; -import mongoose from 'mongoose'; +import { connectToDatabase, disconnectDatabase } from "@/lib/mongoose"; +import mongoose from "mongoose"; -describe('Database Connection (Singleton)', () => { +describe("Database Connection (Singleton)", () => { beforeEach(async () => { // Reset global mongoose state global.mongoose = { conn: null, promise: null }; @@ -11,14 +11,14 @@ describe('Database Connection (Singleton)', () => { await disconnectDatabase(); }); - it('should establish a database connection', async () => { + it("should establish a database connection", async () => { const connection = await connectToDatabase(); expect(connection).toBeDefined(); expect(mongoose.connection.readyState).toBe(1); // 1 = connected }); - it('should return the same connection on multiple calls', async () => { + it("should return the same connection on multiple calls", async () => { const conn1 = await connectToDatabase(); const conn2 = await connectToDatabase(); const conn3 = await connectToDatabase(); @@ -27,7 +27,7 @@ describe('Database Connection (Singleton)', () => { expect(conn2).toBe(conn3); }); - it('should handle concurrent connection requests', async () => { + it("should handle concurrent connection requests", async () => { const promises = Array(10) .fill(null) .map(() => connectToDatabase()); @@ -38,7 +38,7 @@ describe('Database Connection (Singleton)', () => { expect(uniqueConnections.size).toBe(1); }); - it('should reconnect after disconnection', async () => { + it("should reconnect after disconnection", async () => { const conn1 = await connectToDatabase(); expect(mongoose.connection.readyState).toBe(1); @@ -48,4 +48,4 @@ describe('Database Connection (Singleton)', () => { const conn2 = await connectToDatabase(); expect(mongoose.connection.readyState).toBe(1); }); -}); \ No newline at end of file +}); diff --git a/lib/__tests__/services.listings.test.ts b/lib/__tests__/services.listings.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/mongoose.ts b/lib/mongoose.ts index 988ab74..f50cb5a 100644 --- a/lib/mongoose.ts +++ b/lib/mongoose.ts @@ -1,12 +1,13 @@ import mongoose from "mongoose"; -const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null +// const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null -if (!MONGODB_URI) { - throw new Error( - "Please define the DATABASE_URL environment variable inside .env" - ); -} +// if (!MONGODB_URI) { +// throw new Error( +// "Please define the DATABASE_URL environment variable inside .env" +// ); +// } +// move inside the connectDB function so the test script can access type MongooseCache = { conn: typeof mongoose | null; @@ -28,6 +29,13 @@ const cached: MongooseCache = globalForMongoose.mongoose ?? { globalForMongoose.mongoose = cached; export async function connectToDatabase() { + const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null + + if (!MONGODB_URI) { + throw new Error( + "Please define the DATABASE_URL environment variable inside .env" + ); + } if (cached.conn) { return cached.conn; } @@ -58,3 +66,7 @@ export async function disconnectDatabase() { } } } + +function test() { + console.log("not tested"); // making sure coverage can see which aren't tested +} diff --git a/package-lock.json b/package-lock.json index 737f487..5f42d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", "typescript": "^5" } }, @@ -645,6 +646,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -2362,6 +2387,34 @@ } } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3113,6 +3166,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -4059,6 +4125,13 @@ "dev": true, "license": "MIT" }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4295,6 +4368,16 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -10332,6 +10415,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -10586,6 +10720,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -10951,6 +11092,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b1ed7a6..88b34fe 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", "typescript": "^5" } } diff --git a/playwright-report/.last-run.json b/playwright-report/.last-run.json new file mode 100644 index 0000000..cdc6ddf --- /dev/null +++ b/playwright-report/.last-run.json @@ -0,0 +1,6 @@ +{ + "status": "failed", + "failedTests": [ + "ae7629b28126d82233f9-d99f87c5846d54b5baeb" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e7ff3a2..705f5ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, "include": [ @@ -35,7 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From fa740b39866745896f83f89de2be2a86b65f869a Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Sun, 5 Apr 2026 21:24:40 -0700 Subject: [PATCH 09/20] Created several tests for services and api layer, attempting to remove coverage from git tracking --- .gitignore | 3 +- app/api/listings/__tests__/route.test.ts | 251 +++++++ coverage/clover.xml | 131 ---- coverage/coverage-final.json | 4 - .../app/api/listings/[id]/index.html | 116 --- .../app/api/listings/[id]/route.ts.html | 694 ------------------ .../lcov-report/app/api/listings/index.html | 116 --- .../app/api/listings/route.ts.html | 433 ----------- coverage/lcov-report/base.css | 224 ------ coverage/lcov-report/block-navigation.js | 87 --- coverage/lcov-report/example.ts.html | 118 --- coverage/lcov-report/favicon.png | Bin 445 -> 0 bytes coverage/lcov-report/index.html | 146 ---- coverage/lcov-report/mongoose.ts.html | 301 -------- coverage/lcov-report/prettify.css | 1 - coverage/lcov-report/prettify.js | 2 - .../lcov-report/services/listings/index.html | 116 --- .../services/listings/listings.ts.html | 439 ----------- coverage/lcov-report/sort-arrow-sprite.png | Bin 138 -> 0 bytes coverage/lcov-report/sorter.js | 210 ------ coverage/lcov-report/utils.ts.html | 106 --- coverage/lcov.info | 204 ----- jest.config.ts | 24 +- lib/__tests__/listings.id.route.test.ts | 0 lib/__tests__/listings.route.test.ts | 120 --- lib/__tests__/services.listings.test.ts | 0 lib/mongoose.ts | 1 + models/Listing.ts | 4 +- services/listings/__tests__/listings.test.ts | 342 +++++++++ services/listings/listings.ts | 3 +- 30 files changed, 609 insertions(+), 3587 deletions(-) create mode 100644 app/api/listings/__tests__/route.test.ts delete mode 100644 coverage/clover.xml delete mode 100644 coverage/coverage-final.json delete mode 100644 coverage/lcov-report/app/api/listings/[id]/index.html delete mode 100644 coverage/lcov-report/app/api/listings/[id]/route.ts.html delete mode 100644 coverage/lcov-report/app/api/listings/index.html delete mode 100644 coverage/lcov-report/app/api/listings/route.ts.html delete mode 100644 coverage/lcov-report/base.css delete mode 100644 coverage/lcov-report/block-navigation.js delete mode 100644 coverage/lcov-report/example.ts.html delete mode 100644 coverage/lcov-report/favicon.png delete mode 100644 coverage/lcov-report/index.html delete mode 100644 coverage/lcov-report/mongoose.ts.html delete mode 100644 coverage/lcov-report/prettify.css delete mode 100644 coverage/lcov-report/prettify.js delete mode 100644 coverage/lcov-report/services/listings/index.html delete mode 100644 coverage/lcov-report/services/listings/listings.ts.html delete mode 100644 coverage/lcov-report/sort-arrow-sprite.png delete mode 100644 coverage/lcov-report/sorter.js delete mode 100644 coverage/lcov-report/utils.ts.html delete mode 100644 coverage/lcov.info delete mode 100644 lib/__tests__/listings.id.route.test.ts delete mode 100644 lib/__tests__/listings.route.test.ts delete mode 100644 lib/__tests__/services.listings.test.ts create mode 100644 services/listings/__tests__/listings.test.ts diff --git a/.gitignore b/.gitignore index 2b24613..319f118 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ .next/ .env.local .DS_Store -.env \ No newline at end of file +.env +coverage/ \ No newline at end of file diff --git a/app/api/listings/__tests__/route.test.ts b/app/api/listings/__tests__/route.test.ts new file mode 100644 index 0000000..8a82164 --- /dev/null +++ b/app/api/listings/__tests__/route.test.ts @@ -0,0 +1,251 @@ +import mongoose from "mongoose"; +import { GET, POST } from "@/app/api/listings/route"; +import { GET as GET_BY_ID, PUT, DELETE } from "@/app/api/listings/[id]/route"; +import { connectToDatabase } from "@/lib/mongoose"; + +/** test route handler, mock db connection and svc handlers */ +jest.mock("@/lib/mongoose", () => ({ + connectToDatabase: jest.fn(), +})); + +jest.mock("@/services/listings/listings", () => ({ + getListings: jest.fn(), + getFilteredListings: jest.fn(), + getListing: jest.fn(), + addListing: jest.fn(), + updateListing: jest.fn(), + deleteListing: jest.fn(), +})); + +/** import after mocking */ +import { + getListings, + getFilteredListings, + getListing, + addListing, + updateListing, + deleteListing, +} from "@/services/listings/listings"; + +describe("API: Successful Responses", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("GET /listings", () => { + test("returns filtered listings successfully", async () => { + // arrange + const date = new Date().toISOString(); + const id = "123"; + + const listingData = { + id: id, // just keep as string now since res.json() stringifies + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (getFilteredListings as jest.Mock).mockResolvedValue({ + listings: [listingData], + pagination: { page: 1, limit: 10, total: 1, totalPages: 1 }, + }); + + // act + const req = new Request("http://localhost/api/listings"); + const res = await GET(req); + const body = await res.json(); + + // assert + expect(res.status).toBe(200); + expect(body.success).toBe(true); + expect(body.data).toEqual([listingData]); + expect(body.pagination).toEqual({ + page: 1, + limit: 10, + total: 1, + totalPages: 1, + }); + expect(getFilteredListings).toHaveBeenCalledWith({ + labId: undefined, + itemId: undefined, + page: 1, // default + limit: 10, // default + }); + }); + }); + + describe("GET /listings/[id]", () => { + test("returns a specific listing successfully", async () => { + const date = new Date().toISOString(); + const id = new mongoose.Types.ObjectId().toString(); + + const listingData = { + id: id, + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (getListing as jest.Mock).mockResolvedValue(listingData); + + const req = new Request(`http://localhost/api/listings/${id}`); + const res = await GET_BY_ID(req, { params: { id: id } }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.success).toEqual(true); + expect(body.data).toEqual(listingData); + }); + }); + + describe("POST /listings", () => { + test("creates a new listing successfully", async () => {}); + }); + + describe("PUT /listings/[id]", () => { + test("updates a listing successfully", async () => {}); + }); + + describe("DELETE /listings/[id]", () => { + test("deletes a listing successfully", async () => {}); + }); +}); + +describe("API: Error Responses", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("GET /listings", () => { + test("DB connection error", async () => { + (connectToDatabase as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request("http://localhost/api/listings"); + const res = await GET(req); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toEqual(false); + expect(body.message).toEqual("Error connecting to database."); + }); + + test("service error retrieving listings", async () => { + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (getFilteredListings as jest.Mock).mockRejectedValue( + new Error("DB Error") + ); + + const req = new Request("http://localhost/api/listings"); + const res = await GET(req); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toEqual(false); + expect(body.message).toEqual("Error occurred while retrieving listings."); + expect(getFilteredListings).toHaveBeenCalledWith({ + labId: undefined, + itemId: undefined, + page: 1, + limit: 10, + }); + }); + }); + + describe("GET /listings/[id]", () => { + test("DB connection error", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request(`http://localhost/api/listings/${id}`); + const res = await GET_BY_ID(req, { params: { id: id } }); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toEqual(false); + expect(body.message).toEqual("Error connecting to database."); + }); + + test("invalid id format", async () => { + const id = "123"; + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + + const req = new Request(`http://localhost/api/listings/${id}`); + const res = await GET_BY_ID(req, { params: { id: id } }); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.success).toEqual(false); + expect(body.message).toEqual( + "Invalid ID format. Must be a valid MongoDB ObjectId." + ); + }); + + test("listing not found", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (getListing as jest.Mock).mockResolvedValue(null); + + const req = new Request(`http://localhost/api/listings/${id}`); + const res = await GET_BY_ID(req, { params: { id: id } }); + const body = await res.json(); + + expect(res.status).toBe(404); + expect(body.success).toEqual(false); + expect(body.message).toEqual("Listing not found."); + expect(getListing).toHaveBeenCalledWith(id); + }); + + test("service error retrieving listing", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (getListing as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request(`http://localhost/api/listings/${id}`); + const res = await GET_BY_ID(req, { params: { id: id } }); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toEqual(false); + expect(body.message).toEqual("Error occurred while retrieving listing."); + expect(getListing).toHaveBeenCalledWith(id); + }); + }); + + describe("POST /listings", () => { + test("DB connection error", async () => {}); + test("invalid req body", async () => {}); + test("listing already exists", async () => {}); + test("service error creating listing", async () => {}); + }); + + describe("PUT /listings/[id]", () => { + test("DB connection error", async () => {}); + test("invalid id format", async () => {}); + test("invalid req body", async () => {}); + test("listing not found", async () => {}); + test("service error updating listing", async () => {}); + }); + + describe("DELETE /listings/[id]", () => { + test("DB connection error", async () => {}); + test("invalid id format", async () => {}); + test("listing not found", async () => {}); + test("service error deleting listing", async () => {}); + }); +}); diff --git a/coverage/clover.xml b/coverage/clover.xml deleted file mode 100644 index 30f45d5..0000000 --- a/coverage/clover.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json deleted file mode 100644 index b4df2a4..0000000 --- a/coverage/coverage-final.json +++ /dev/null @@ -1,4 +0,0 @@ -{"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/route.ts": {"path":"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/route.ts","statementMap":{"0":{"start":{"line":116,"column":9},"end":{"line":116,"column":12}},"1":{"start":{"line":116,"column":14},"end":{"line":116,"column":18}},"2":{"start":{"line":1,"column":0},"end":{"line":1,"column":43}},"3":{"start":{"line":2,"column":0},"end":{"line":2,"column":51}},"4":{"start":{"line":3,"column":0},"end":{"line":3,"column":24}},"5":{"start":{"line":4,"column":0},"end":{"line":4,"column":79}},"6":{"start":{"line":7,"column":32},"end":{"line":12,"column":2}},"7":{"start":{"line":21,"column":2},"end":{"line":28,"column":3}},"8":{"start":{"line":22,"column":4},"end":{"line":22,"column":30}},"9":{"start":{"line":24,"column":4},"end":{"line":27,"column":6}},"10":{"start":{"line":30,"column":27},"end":{"line":30,"column":47}},"11":{"start":{"line":31,"column":16},"end":{"line":31,"column":54}},"12":{"start":{"line":32,"column":17},"end":{"line":32,"column":56}},"13":{"start":{"line":33,"column":15},"end":{"line":33,"column":56}},"14":{"start":{"line":34,"column":16},"end":{"line":34,"column":59}},"15":{"start":{"line":36,"column":2},"end":{"line":57,"column":3}},"16":{"start":{"line":37,"column":37},"end":{"line":42,"column":6}},"17":{"start":{"line":44,"column":4},"end":{"line":51,"column":6}},"18":{"start":{"line":53,"column":4},"end":{"line":56,"column":6}},"19":{"start":{"line":66,"column":2},"end":{"line":73,"column":3}},"20":{"start":{"line":67,"column":4},"end":{"line":67,"column":30}},"21":{"start":{"line":69,"column":4},"end":{"line":72,"column":6}},"22":{"start":{"line":77,"column":15},"end":{"line":77,"column":35}},"23":{"start":{"line":78,"column":21},"end":{"line":78,"column":60}},"24":{"start":{"line":80,"column":2},"end":{"line":88,"column":3}},"25":{"start":{"line":81,"column":4},"end":{"line":87,"column":6}},"26":{"start":{"line":90,"column":2},"end":{"line":113,"column":3}},"27":{"start":{"line":91,"column":21},"end":{"line":91,"column":69}},"28":{"start":{"line":92,"column":20},"end":{"line":92,"column":49}},"29":{"start":{"line":93,"column":4},"end":{"line":100,"column":6}},"30":{"start":{"line":102,"column":4},"end":{"line":108,"column":5}},"31":{"start":{"line":103,"column":6},"end":{"line":107,"column":8}},"32":{"start":{"line":109,"column":4},"end":{"line":112,"column":6}}},"fnMap":{"0":{"name":"GET","decl":{"start":{"line":20,"column":15},"end":{"line":20,"column":18}},"loc":{"start":{"line":20,"column":35},"end":{"line":58,"column":1}}},"1":{"name":"POST","decl":{"start":{"line":65,"column":15},"end":{"line":65,"column":19}},"loc":{"start":{"line":65,"column":36},"end":{"line":114,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":31,"column":16},"end":{"line":31,"column":54}},"type":"binary-expr","locations":[{"start":{"line":31,"column":16},"end":{"line":31,"column":41}},{"start":{"line":31,"column":45},"end":{"line":31,"column":54}}]},"1":{"loc":{"start":{"line":32,"column":17},"end":{"line":32,"column":56}},"type":"binary-expr","locations":[{"start":{"line":32,"column":17},"end":{"line":32,"column":43}},{"start":{"line":32,"column":47},"end":{"line":32,"column":56}}]},"2":{"loc":{"start":{"line":33,"column":24},"end":{"line":33,"column":55}},"type":"binary-expr","locations":[{"start":{"line":33,"column":24},"end":{"line":33,"column":48}},{"start":{"line":33,"column":52},"end":{"line":33,"column":55}}]},"3":{"loc":{"start":{"line":34,"column":25},"end":{"line":34,"column":58}},"type":"binary-expr","locations":[{"start":{"line":34,"column":25},"end":{"line":34,"column":50}},{"start":{"line":34,"column":54},"end":{"line":34,"column":58}}]},"4":{"loc":{"start":{"line":80,"column":2},"end":{"line":88,"column":3}},"type":"if","locations":[{"start":{"line":80,"column":2},"end":{"line":88,"column":3}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":102,"column":4},"end":{"line":108,"column":5}},"type":"if","locations":[{"start":{"line":102,"column":4},"end":{"line":108,"column":5}},{"start":{},"end":{}}]}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0},"f":{"0":0,"1":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0]}} -,"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/[id]/route.ts": {"path":"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/app/api/listings/[id]/route.ts","statementMap":{"0":{"start":{"line":203,"column":9},"end":{"line":203,"column":12}},"1":{"start":{"line":203,"column":14},"end":{"line":203,"column":17}},"2":{"start":{"line":203,"column":19},"end":{"line":203,"column":25}},"3":{"start":{"line":1,"column":0},"end":{"line":1,"column":43}},"4":{"start":{"line":2,"column":0},"end":{"line":2,"column":51}},"5":{"start":{"line":3,"column":0},"end":{"line":3,"column":24}},"6":{"start":{"line":4,"column":0},"end":{"line":4,"column":null}},"7":{"start":{"line":11,"column":23},"end":{"line":13,"column":57}},"8":{"start":{"line":14,"column":32},"end":{"line":19,"column":2}},"9":{"start":{"line":28,"column":2},"end":{"line":35,"column":3}},"10":{"start":{"line":29,"column":4},"end":{"line":29,"column":30}},"11":{"start":{"line":31,"column":4},"end":{"line":34,"column":6}},"12":{"start":{"line":37,"column":19},"end":{"line":37,"column":54}},"13":{"start":{"line":38,"column":2},"end":{"line":46,"column":3}},"14":{"start":{"line":39,"column":4},"end":{"line":45,"column":6}},"15":{"start":{"line":48,"column":2},"end":{"line":62,"column":3}},"16":{"start":{"line":49,"column":20},"end":{"line":49,"column":51}},"17":{"start":{"line":50,"column":4},"end":{"line":55,"column":5}},"18":{"start":{"line":51,"column":6},"end":{"line":54,"column":8}},"19":{"start":{"line":56,"column":4},"end":{"line":56,"column":80}},"20":{"start":{"line":58,"column":4},"end":{"line":61,"column":6}},"21":{"start":{"line":71,"column":2},"end":{"line":78,"column":3}},"22":{"start":{"line":72,"column":4},"end":{"line":72,"column":30}},"23":{"start":{"line":74,"column":4},"end":{"line":77,"column":6}},"24":{"start":{"line":80,"column":19},"end":{"line":80,"column":54}},"25":{"start":{"line":81,"column":2},"end":{"line":89,"column":3}},"26":{"start":{"line":82,"column":4},"end":{"line":88,"column":6}},"27":{"start":{"line":91,"column":15},"end":{"line":91,"column":35}},"28":{"start":{"line":93,"column":20},"end":{"line":96,"column":4}},"29":{"start":{"line":98,"column":24},"end":{"line":101,"column":4}},"30":{"start":{"line":102,"column":2},"end":{"line":110,"column":3}},"31":{"start":{"line":103,"column":4},"end":{"line":109,"column":6}},"32":{"start":{"line":112,"column":2},"end":{"line":142,"column":3}},"33":{"start":{"line":113,"column":27},"end":{"line":115,"column":null}},"34":{"start":{"line":117,"column":4},"end":{"line":125,"column":5}},"35":{"start":{"line":118,"column":6},"end":{"line":124,"column":8}},"36":{"start":{"line":126,"column":4},"end":{"line":133,"column":6}},"37":{"start":{"line":135,"column":4},"end":{"line":141,"column":6}},"38":{"start":{"line":154,"column":2},"end":{"line":161,"column":3}},"39":{"start":{"line":155,"column":4},"end":{"line":155,"column":30}},"40":{"start":{"line":157,"column":4},"end":{"line":160,"column":6}},"41":{"start":{"line":163,"column":19},"end":{"line":163,"column":54}},"42":{"start":{"line":164,"column":2},"end":{"line":172,"column":3}},"43":{"start":{"line":165,"column":4},"end":{"line":171,"column":6}},"44":{"start":{"line":174,"column":2},"end":{"line":200,"column":3}},"45":{"start":{"line":175,"column":20},"end":{"line":175,"column":54}},"46":{"start":{"line":176,"column":4},"end":{"line":184,"column":5}},"47":{"start":{"line":177,"column":6},"end":{"line":183,"column":8}},"48":{"start":{"line":185,"column":4},"end":{"line":191,"column":6}},"49":{"start":{"line":193,"column":4},"end":{"line":199,"column":6}}},"fnMap":{"0":{"name":"GET","decl":{"start":{"line":27,"column":15},"end":{"line":27,"column":18}},"loc":{"start":{"line":27,"column":75},"end":{"line":63,"column":1}}},"1":{"name":"PUT","decl":{"start":{"line":70,"column":15},"end":{"line":70,"column":18}},"loc":{"start":{"line":70,"column":75},"end":{"line":143,"column":1}}},"2":{"name":"DELETE","decl":{"start":{"line":150,"column":15},"end":{"line":150,"column":21}},"loc":{"start":{"line":152,"column":40},"end":{"line":201,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":38,"column":2},"end":{"line":46,"column":3}},"type":"if","locations":[{"start":{"line":38,"column":2},"end":{"line":46,"column":3}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":50,"column":4},"end":{"line":55,"column":5}},"type":"if","locations":[{"start":{"line":50,"column":4},"end":{"line":55,"column":5}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":81,"column":2},"end":{"line":89,"column":3}},"type":"if","locations":[{"start":{"line":81,"column":2},"end":{"line":89,"column":3}},{"start":{},"end":{}}]},"3":{"loc":{"start":{"line":102,"column":2},"end":{"line":110,"column":3}},"type":"if","locations":[{"start":{"line":102,"column":2},"end":{"line":110,"column":3}},{"start":{},"end":{}}]},"4":{"loc":{"start":{"line":117,"column":4},"end":{"line":125,"column":5}},"type":"if","locations":[{"start":{"line":117,"column":4},"end":{"line":125,"column":5}},{"start":{},"end":{}}]},"5":{"loc":{"start":{"line":164,"column":2},"end":{"line":172,"column":3}},"type":"if","locations":[{"start":{"line":164,"column":2},"end":{"line":172,"column":3}},{"start":{},"end":{}}]},"6":{"loc":{"start":{"line":176,"column":4},"end":{"line":184,"column":5}},"type":"if","locations":[{"start":{"line":176,"column":4},"end":{"line":184,"column":5}},{"start":{},"end":{}}]}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0},"f":{"0":0,"1":0,"2":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0]}} -,"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/services/listings/listings.ts": {"path":"/Users/amormiovelasquez/Desktop/cses_dev/lims-lab-inventory/services/listings/listings.ts","statementMap":{"0":{"start":{"line":113,"column":2},"end":{"line":113,"column":21}},"1":{"start":{"line":114,"column":2},"end":{"line":114,"column":12}},"2":{"start":{"line":115,"column":2},"end":{"line":115,"column":12}},"3":{"start":{"line":116,"column":2},"end":{"line":116,"column":15}},"4":{"start":{"line":117,"column":2},"end":{"line":117,"column":15}},"5":{"start":{"line":2,"column":0},"end":{"line":2,"column":71}},"6":{"start":{"line":5,"column":18},"end":{"line":5,"column":76}},"7":{"start":{"line":5,"column":53},"end":{"line":5,"column":76}},"8":{"start":{"line":19,"column":19},"end":{"line":19,"column":51}},"9":{"start":{"line":20,"column":2},"end":{"line":20,"column":55}},"10":{"start":{"line":20,"column":35},"end":{"line":20,"column":53}},"11":{"start":{"line":33,"column":21},"end":{"line":33,"column":23}},"12":{"start":{"line":34,"column":2},"end":{"line":34,"column":33}},"13":{"start":{"line":34,"column":13},"end":{"line":34,"column":33}},"14":{"start":{"line":35,"column":2},"end":{"line":35,"column":36}},"15":{"start":{"line":35,"column":14},"end":{"line":35,"column":36}},"16":{"start":{"line":37,"column":20},"end":{"line":37,"column":22}},"17":{"start":{"line":38,"column":20},"end":{"line":38,"column":54}},"18":{"start":{"line":40,"column":4},"end":{"line":40,"column":63}},"19":{"start":{"line":41,"column":15},"end":{"line":41,"column":38}},"20":{"start":{"line":43,"column":28},"end":{"line":50,"column":4}},"21":{"start":{"line":52,"column":2},"end":{"line":60,"column":4}},"22":{"start":{"line":53,"column":40},"end":{"line":53,"column":58}},"23":{"start":{"line":69,"column":18},"end":{"line":69,"column":56}},"24":{"start":{"line":70,"column":2},"end":{"line":70,"column":45}},"25":{"start":{"line":79,"column":25},"end":{"line":79,"column":62}},"26":{"start":{"line":80,"column":2},"end":{"line":80,"column":35}},"27":{"start":{"line":93,"column":25},"end":{"line":96,"column":11}},"28":{"start":{"line":97,"column":2},"end":{"line":97,"column":59}},"29":{"start":{"line":108,"column":18},"end":{"line":108,"column":65}},"30":{"start":{"line":109,"column":2},"end":{"line":109,"column":26}}},"fnMap":{"0":{"name":"(anonymous_1)","decl":{"start":{"line":5,"column":18},"end":{"line":5,"column":19}},"loc":{"start":{"line":5,"column":53},"end":{"line":5,"column":76}}},"1":{"name":"getListings","decl":{"start":{"line":18,"column":15},"end":{"line":18,"column":26}},"loc":{"start":{"line":18,"column":26},"end":{"line":21,"column":1}}},"2":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":22},"end":{"line":20,"column":23}},"loc":{"start":{"line":20,"column":35},"end":{"line":20,"column":53}}},"3":{"name":"getFilteredListings","decl":{"start":{"line":27,"column":15},"end":{"line":27,"column":34}},"loc":{"start":{"line":32,"column":15},"end":{"line":61,"column":1}}},"4":{"name":"(anonymous_5)","decl":{"start":{"line":53,"column":27},"end":{"line":53,"column":28}},"loc":{"start":{"line":53,"column":40},"end":{"line":53,"column":58}}},"5":{"name":"getListing","decl":{"start":{"line":68,"column":15},"end":{"line":68,"column":25}},"loc":{"start":{"line":68,"column":36},"end":{"line":71,"column":1}}},"6":{"name":"addListing","decl":{"start":{"line":78,"column":15},"end":{"line":78,"column":25}},"loc":{"start":{"line":78,"column":50},"end":{"line":81,"column":1}}},"7":{"name":"updateListing","decl":{"start":{"line":89,"column":15},"end":{"line":89,"column":28}},"loc":{"start":{"line":91,"column":29},"end":{"line":98,"column":1}}},"8":{"name":"deleteListing","decl":{"start":{"line":107,"column":15},"end":{"line":107,"column":28}},"loc":{"start":{"line":107,"column":39},"end":{"line":110,"column":1}}}},"branchMap":{"0":{"loc":{"start":{"line":34,"column":2},"end":{"line":34,"column":33}},"type":"if","locations":[{"start":{"line":34,"column":2},"end":{"line":34,"column":33}},{"start":{},"end":{}}]},"1":{"loc":{"start":{"line":35,"column":2},"end":{"line":35,"column":36}},"type":"if","locations":[{"start":{"line":35,"column":2},"end":{"line":35,"column":36}},{"start":{},"end":{}}]},"2":{"loc":{"start":{"line":38,"column":20},"end":{"line":38,"column":54}},"type":"cond-expr","locations":[{"start":{"line":38,"column":46},"end":{"line":38,"column":47}},{"start":{"line":38,"column":50},"end":{"line":38,"column":54}}]},"3":{"loc":{"start":{"line":38,"column":20},"end":{"line":38,"column":43}},"type":"binary-expr","locations":[{"start":{"line":38,"column":20},"end":{"line":38,"column":31}},{"start":{"line":38,"column":35},"end":{"line":38,"column":43}}]},"4":{"loc":{"start":{"line":40,"column":4},"end":{"line":40,"column":63}},"type":"cond-expr","locations":[{"start":{"line":40,"column":32},"end":{"line":40,"column":34}},{"start":{"line":40,"column":37},"end":{"line":40,"column":63}}]},"5":{"loc":{"start":{"line":40,"column":4},"end":{"line":40,"column":29}},"type":"binary-expr","locations":[{"start":{"line":40,"column":4},"end":{"line":40,"column":16}},{"start":{"line":40,"column":20},"end":{"line":40,"column":29}}]},"6":{"loc":{"start":{"line":70,"column":9},"end":{"line":70,"column":44}},"type":"cond-expr","locations":[{"start":{"line":70,"column":19},"end":{"line":70,"column":37}},{"start":{"line":70,"column":40},"end":{"line":70,"column":44}}]},"7":{"loc":{"start":{"line":97,"column":9},"end":{"line":97,"column":58}},"type":"cond-expr","locations":[{"start":{"line":97,"column":26},"end":{"line":97,"column":51}},{"start":{"line":97,"column":54},"end":{"line":97,"column":58}}]}},"s":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0},"f":{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0},"b":{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0]}} -} diff --git a/coverage/lcov-report/app/api/listings/[id]/index.html b/coverage/lcov-report/app/api/listings/[id]/index.html deleted file mode 100644 index 9de2934..0000000 --- a/coverage/lcov-report/app/api/listings/[id]/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for app/api/listings/[id] - - - - - - - - - -
-
-

All files app/api/listings/[id]

-
- -
- 0% - Statements - 0/50 -
- - -
- 0% - Branches - 0/14 -
- - -
- 0% - Functions - 0/3 -
- - -
- 0% - Lines - 0/48 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
route.ts -
-
0%0/500%0/140%0/30%0/48
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/app/api/listings/[id]/route.ts.html b/coverage/lcov-report/app/api/listings/[id]/route.ts.html deleted file mode 100644 index 34e5871..0000000 --- a/coverage/lcov-report/app/api/listings/[id]/route.ts.html +++ /dev/null @@ -1,694 +0,0 @@ - - - - - - Code coverage report for app/api/listings/[id]/route.ts - - - - - - - - - -
-
-

All files / app/api/listings/[id] route.ts

-
- -
- 0% - Statements - 0/50 -
- - -
- 0% - Branches - 0/14 -
- - -
- 0% - Functions - 0/3 -
- - -
- 0% - Lines - 0/48 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
import { NextResponse } from "next/server";
-import { connectToDatabase } from "@/lib/mongoose";
-import { z } from "zod";
-import {
-  deleteListing,
-  getListing,
-  updateListing,
-} from "@/services/listings/listings";
- 
-/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */
-const objectIdSchema = z
-  .string()
-  .regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId");
-const listingValidationSchema = z.object({
-  itemId: z.string().min(1),
-  labId: z.string().min(1),
-  quantityAvailable: z.number().min(1),
-  status: z.enum(["ACTIVE", "INACTIVE"]),
-});
- 
-/**
- * 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 } }) {
-  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 } }) {
-  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 body = await request.json();
- 
-  const validator = z.object({
-    id: objectIdSchema,
-    update: listingValidationSchema.partial(),
-  });
- 
-  const parsedRequest = validator.safeParse({
-    id: parsedId.data,
-    update: body,
-  });
-  if (!parsedRequest.success) {
-    return NextResponse.json(
-      {
-        success: false,
-        message: "Invalid request body.",
-      },
-      { status: 400 }
-    );
-  }
- 
-  try {
-    const updatedListing = await updateListing(
-      parsedId.data,
-      parsedRequest.data.update
-    );
-    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 } }
-) {
-  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: 204 }
-    );
-  } catch {
-    return NextResponse.json(
-      {
-        success: false,
-        message: "Error occurred while deleting listing.",
-      },
-      { status: 500 }
-    );
-  }
-}
- 
-export { GET, PUT, DELETE };
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/app/api/listings/index.html b/coverage/lcov-report/app/api/listings/index.html deleted file mode 100644 index 9c23506..0000000 --- a/coverage/lcov-report/app/api/listings/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for app/api/listings - - - - - - - - - -
-
-

All files app/api/listings

-
- -
- 0% - Statements - 0/33 -
- - -
- 0% - Branches - 0/12 -
- - -
- 0% - Functions - 0/2 -
- - -
- 0% - Lines - 0/32 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
route.ts -
-
0%0/330%0/120%0/20%0/32
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/app/api/listings/route.ts.html b/coverage/lcov-report/app/api/listings/route.ts.html deleted file mode 100644 index 9454931..0000000 --- a/coverage/lcov-report/app/api/listings/route.ts.html +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - Code coverage report for app/api/listings/route.ts - - - - - - - - - -
-
-

All files / app/api/listings route.ts

-
- -
- 0% - Statements - 0/33 -
- - -
- 0% - Branches - 0/12 -
- - -
- 0% - Functions - 0/2 -
- - -
- 0% - Lines - 0/32 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
import { NextResponse } from "next/server";
-import { connectToDatabase } from "@/lib/mongoose";
-import { z } from "zod";
-import { getFilteredListings, addListing } from "@/services/listings/listings";
- 
-/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */
-const listingValidationSchema = z.object({
-  itemId: z.string().min(1),
-  labId: z.string().min(1),
-  quantityAvailable: z.number().min(1),
-  status: z.enum(["ACTIVE", "INACTIVE"]),
-});
- 
-/**
- * Get filtered listings stored in db
- * @param request the request
- * ex req: GET /listings/?labId=3&page=2&limit=5 HTTP/1.1
- * @returns JSON response with the filtered listings as JS objects
- */
-async function GET(request: Request) {
-  try {
-    await connectToDatabase();
-  } catch {
-    return NextResponse.json(
-      { success: false, message: "Error connecting to database." },
-      { status: 500 }
-    );
-  }
- 
-  const { searchParams } = new URL(request.url);
-  const labId = searchParams.get("labId") || undefined;
-  const itemId = searchParams.get("itemId") || undefined;
-  const page = parseInt(searchParams.get("page") || "1");
-  const limit = parseInt(searchParams.get("limit") || "10");
- 
-  try {
-    const { listings, pagination } = await getFilteredListings({
-      labId,
-      itemId,
-      page,
-      limit,
-    });
- 
-    return NextResponse.json(
-      {
-        success: true,
-        data: listings,
-        pagination,
-      },
-      { status: 200 }
-    );
-  } catch {
-    return NextResponse.json(
-      { success: false, message: "Error occurred while retrieving listings." },
-      { status: 500 }
-    );
-  }
-}
- 
-/**
- * Create a new listing to store in db
- * @param request the request with JSON data in req body
- * @returns JSON response with success message and req body echoed
- */
-async function POST(request: Request) {
-  try {
-    await connectToDatabase();
-  } catch {
-    return NextResponse.json(
-      { success: false, message: "Error connecting to database." },
-      { status: 500 }
-    );
-  }
- 
-  // assuming frontend sends req with content-type set to app/json
-  // content type automatically set as app/json
-  const body = await request.json();
-  const parsedBody = listingValidationSchema.safeParse(body);
- 
-  if (!parsedBody.success) {
-    return NextResponse.json(
-      {
-        success: false,
-        message: "Invalid request body.",
-      },
-      { status: 400 }
-    );
-  }
- 
-  try {
-    const listingData = { ...parsedBody.data, createdAt: new Date() };
-    const listing = await addListing(listingData);
-    return NextResponse.json(
-      {
-        success: true,
-        message: "Successfully created new listing.",
-        data: listing,
-      },
-      { status: 201, headers: { Location: `/app/listings/${listing.id}` } }
-    );
-  } catch (error: any) {
-    if (error.code === 11000) {
-      return NextResponse.json(
-        // don't send mongo's error - exposes design info
-        { success: false, message: "This listing already exists." },
-        { status: 409 }
-      );
-    }
-    return NextResponse.json(
-      { success: false, message: "Error occurred while creating new listing." },
-      { status: 500 }
-    );
-  }
-}
- 
-export { GET, POST };
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/coverage/lcov-report/example.ts.html b/coverage/lcov-report/example.ts.html deleted file mode 100644 index 469a6a0..0000000 --- a/coverage/lcov-report/example.ts.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - Code coverage report for example.ts - - - - - - - - - -
-
-

All files example.ts

-
- -
- 0% - Statements - 0/2 -
- - -
- 100% - Branches - 0/0 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/2 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12  -  -  -  -  -  -  -  -  -  -  - 
// Example
- 
-/**
- * A utility function that generates a greeting message.
- *
- * @param name - The name to greet.
- * @returns A greeting message.
- */
-export function greetUser(name: string): string {
-    return `Hello, ${name}! Welcome to our site.`;
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 0% - Statements - 0/114 -
- - -
- 0% - Branches - 0/42 -
- - -
- 0% - Functions - 0/14 -
- - -
- 0% - Lines - 0/107 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
app/api/listings -
-
0%0/330%0/120%0/20%0/32
app/api/listings/[id] -
-
0%0/500%0/140%0/30%0/48
services/listings -
-
0%0/310%0/160%0/90%0/27
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/mongoose.ts.html b/coverage/lcov-report/mongoose.ts.html deleted file mode 100644 index a1ecdd4..0000000 --- a/coverage/lcov-report/mongoose.ts.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - - - Code coverage report for mongoose.ts - - - - - - - - - -
-
-

All files mongoose.ts

-
- -
- 83.33% - Statements - 20/24 -
- - -
- 75% - Branches - 9/12 -
- - -
- 66.66% - Functions - 2/3 -
- - -
- 83.33% - Lines - 20/24 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -731x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -1x -  -  -  -1x -  -1x -16x -  -16x -  -  -  -  -16x -2x -  -  -14x -5x -  -  -  -  -14x -14x -  -  -  -  -  -1x -5x -5x -5x -5x -5x -5x -  -  -  -  -  -  -  -  -  -  - 
import mongoose from "mongoose";
- 
-// const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null
- 
-// if (!MONGODB_URI) {
-//   throw new Error(
-//     "Please define the DATABASE_URL environment variable inside .env"
-//   );
-// }
-// move inside the connectDB function so the test script can access
- 
-type MongooseCache = {
-  conn: typeof mongoose | null;
-  promise: Promise<typeof mongoose> | null;
-};
- 
-declare global {
-  // eslint-disable-next-line no-var
-  var mongoose: MongooseCache | undefined;
-}
- 
-const globalForMongoose = globalThis as typeof globalThis & {
-  mongoose?: MongooseCache;
-};
-const cached: MongooseCache = globalForMongoose.mongoose ?? {
-  conn: null,
-  promise: null,
-};
-globalForMongoose.mongoose = cached;
- 
-export async function connectToDatabase() {
-  const MONGODB_URI = process.env.DATABASE_URL!; // assert that db_url is not null
- 
-  Iif (!MONGODB_URI) {
-    throw new Error(
-      "Please define the DATABASE_URL environment variable inside .env"
-    );
-  }
-  if (cached.conn) {
-    return cached.conn;
-  }
- 
-  if (!cached.promise) {
-    cached.promise = mongoose.connect(MONGODB_URI, {
-      bufferCommands: false,
-    });
-  }
- 
-  cached.conn = await cached.promise;
-  return cached.conn;
-}
- 
-/**
- * Disconnect from MongoDB for testing
- */
-export async function disconnectDatabase() {
-  Eif (cached.conn) {
-    try {
-      await cached.conn.disconnect();
-      cached.conn = null;
-      cached.promise = null;
-      console.log("DB disconnected");
-    } catch (error) {
-      console.error("Error disconnecting from database", error);
-      throw error;
-    }
-  }
-}
- 
-function test() {
-  console.log("not tested"); // making sure coverage can see which aren't tested
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov-report/services/listings/index.html b/coverage/lcov-report/services/listings/index.html deleted file mode 100644 index 6c9eb88..0000000 --- a/coverage/lcov-report/services/listings/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for services/listings - - - - - - - - - -
-
-

All files services/listings

-
- -
- 0% - Statements - 0/31 -
- - -
- 0% - Branches - 0/16 -
- - -
- 0% - Functions - 0/9 -
- - -
- 0% - Lines - 0/27 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
listings.ts -
-
0%0/310%0/160%0/90%0/27
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/services/listings/listings.ts.html b/coverage/lcov-report/services/listings/listings.ts.html deleted file mode 100644 index 5b05e85..0000000 --- a/coverage/lcov-report/services/listings/listings.ts.html +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - Code coverage report for services/listings/listings.ts - - - - - - - - - -
-
-

All files / services/listings listings.ts

-
- -
- 0% - Statements - 0/31 -
- - -
- 0% - Branches - 0/16 -
- - -
- 0% - Functions - 0/9 -
- - -
- 0% - Lines - 0/27 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
import type { HydratedDocument } from "mongoose";
-import ListingModel, { Listing, ListingInput } from "@/models/Listing";
- 
-type ListingDocument = HydratedDocument<ListingInput>;
-const toListing = (doc: ListingDocument): Listing => doc.toObject<Listing>();
- 
-interface FilterParams {
-  labId?: string;
-  itemId?: string;
-  page: number;
-  limit: number;
-}
- 
-/**
- * Get all listing entries (likely unused since filtered & paginated more realistic)
- * @returns array of listings as JS objects
- */
-async function getListings(): Promise<Listing[]> {
-  const listings = await ListingModel.find().exec();
-  return listings.map((listing) => toListing(listing));
-}
- 
-/**
- * Get filtered listing entries
- * @returns array of listings as JS objects
- */
-async function getFilteredListings({
-  labId,
-  itemId,
-  page,
-  limit,
-}: FilterParams) {
-  const query: any = {};
-  if (labId) query.labId = labId;
-  if (itemId) query.itemId = itemId;
- 
-  const MAX_LIMIT = 20; // inquire about this in the future
-  const validPage = isNaN(page) || page < 1 ? 1 : page;
-  const validLimit =
-    isNaN(limit) || limit < 1 ? 10 : Math.min(limit, MAX_LIMIT);
-  const skip = (page - 1) * validLimit;
- 
-  const [listings, total] = await Promise.all([
-    ListingModel.find(query)
-      .sort({ createdAt: -1 }) // Sort from newest to oldest
-      .skip(skip)
-      .limit(validLimit)
-      .exec(), // toListing handles doc to JS object, so lean unncessary
-    ListingModel.countDocuments(query),
-  ]);
- 
-  return {
-    listings: listings.map((listing) => toListing(listing)),
-    pagination: {
-      page: validPage,
-      limit: validLimit,
-      total,
-      totalPages: Math.ceil(total / validLimit),
-    },
-  };
-}
- 
-/**
- * Get a listing entry by ID
- * @param id the ID of the listing to get
- * @returns the listing as a JS object
- */
-async function getListing(id: string): Promise<Listing | null> {
-  const listing = await ListingModel.findById(id).exec();
-  return listing ? toListing(listing) : null;
-}
- 
-/**
- * Add new listing entry
- * @param newListing the listing data to add
- * @returns the created listing as JS object
- */
-async function addListing(newListing: ListingInput): Promise<Listing> {
-  const createdListing = await ListingModel.create(newListing);
-  return toListing(createdListing);
-}
- 
-/**
- * Update listing entry by ID
- * @param id the ID of the listing to update
- * @param data the data passed in to partially or fully update the listing
- * @returns the updated listing or null if not found
- */
-async function updateListing(
-  id: string,
-  data: Partial<ListingInput>
-): Promise<Listing | null> {
-  const updatedListing = await ListingModel.findByIdAndUpdate(id, data, {
-    new: true,
-    runValidators: true,
-  }).exec();
-  return updatedListing ? toListing(updatedListing) : null;
-}
- 
-/**
- * Delete a listing entry by ID
- * @param id the ID of the listing to delete
- * @returns true if the listing was deleted, false otherwise
- */
-// DON'T use this for tables that you don't actually need to potentially delete things from
-// Could be used accidentally or misused maliciously to get rid of important data
-async function deleteListing(id: string): Promise<boolean> {
-  const deleted = await ListingModel.findByIdAndDelete(id).exec();
-  return Boolean(deleted);
-}
- 
-export {
-  getFilteredListings,
-  getListing,
-  addListing,
-  updateListing,
-  deleteListing,
-};
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov-report/sort-arrow-sprite.png b/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/coverage/lcov-report/sorter.js b/coverage/lcov-report/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/coverage/lcov-report/utils.ts.html b/coverage/lcov-report/utils.ts.html deleted file mode 100644 index 5b4571a..0000000 --- a/coverage/lcov-report/utils.ts.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - Code coverage report for utils.ts - - - - - - - - - -
-
-

All files utils.ts

-
- -
- 0% - Statements - 0/4 -
- - -
- 100% - Branches - 0/0 -
- - -
- 0% - Functions - 0/1 -
- - -
- 0% - Lines - 0/4 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8  -  -  -  -  -  -  - 
import { clsx } from "clsx";
-import type { ClassValue } from "clsx";
-import { twMerge } from "tailwind-merge";
- 
-export function cn(...inputs: ClassValue[]) {
-    return twMerge(clsx(inputs));
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info deleted file mode 100644 index 50418b9..0000000 --- a/coverage/lcov.info +++ /dev/null @@ -1,204 +0,0 @@ -TN: -SF:app/api/listings/route.ts -FN:20,GET -FN:65,POST -FNF:2 -FNH:0 -FNDA:0,GET -FNDA:0,POST -DA:1,0 -DA:2,0 -DA:3,0 -DA:4,0 -DA:7,0 -DA:21,0 -DA:22,0 -DA:24,0 -DA:30,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:36,0 -DA:37,0 -DA:44,0 -DA:53,0 -DA:66,0 -DA:67,0 -DA:69,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:81,0 -DA:90,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:102,0 -DA:103,0 -DA:109,0 -DA:116,0 -LF:32 -LH:0 -BRDA:31,0,0,0 -BRDA:31,0,1,0 -BRDA:32,1,0,0 -BRDA:32,1,1,0 -BRDA:33,2,0,0 -BRDA:33,2,1,0 -BRDA:34,3,0,0 -BRDA:34,3,1,0 -BRDA:80,4,0,0 -BRDA:80,4,1,0 -BRDA:102,5,0,0 -BRDA:102,5,1,0 -BRF:12 -BRH:0 -end_of_record -TN: -SF:app/api/listings/[id]/route.ts -FN:27,GET -FN:70,PUT -FN:150,DELETE -FNF:3 -FNH:0 -FNDA:0,GET -FNDA:0,PUT -FNDA:0,DELETE -DA:1,0 -DA:2,0 -DA:3,0 -DA:4,0 -DA:11,0 -DA:14,0 -DA:28,0 -DA:29,0 -DA:31,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:48,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:56,0 -DA:58,0 -DA:71,0 -DA:72,0 -DA:74,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:91,0 -DA:93,0 -DA:98,0 -DA:102,0 -DA:103,0 -DA:112,0 -DA:113,0 -DA:117,0 -DA:118,0 -DA:126,0 -DA:135,0 -DA:154,0 -DA:155,0 -DA:157,0 -DA:163,0 -DA:164,0 -DA:165,0 -DA:174,0 -DA:175,0 -DA:176,0 -DA:177,0 -DA:185,0 -DA:193,0 -DA:203,0 -LF:48 -LH:0 -BRDA:38,0,0,0 -BRDA:38,0,1,0 -BRDA:50,1,0,0 -BRDA:50,1,1,0 -BRDA:81,2,0,0 -BRDA:81,2,1,0 -BRDA:102,3,0,0 -BRDA:102,3,1,0 -BRDA:117,4,0,0 -BRDA:117,4,1,0 -BRDA:164,5,0,0 -BRDA:164,5,1,0 -BRDA:176,6,0,0 -BRDA:176,6,1,0 -BRF:14 -BRH:0 -end_of_record -TN: -SF:services/listings/listings.ts -FN:5,(anonymous_1) -FN:18,getListings -FN:20,(anonymous_3) -FN:27,getFilteredListings -FN:53,(anonymous_5) -FN:68,getListing -FN:78,addListing -FN:89,updateListing -FN:107,deleteListing -FNF:9 -FNH:0 -FNDA:0,(anonymous_1) -FNDA:0,getListings -FNDA:0,(anonymous_3) -FNDA:0,getFilteredListings -FNDA:0,(anonymous_5) -FNDA:0,getListing -FNDA:0,addListing -FNDA:0,updateListing -FNDA:0,deleteListing -DA:2,0 -DA:5,0 -DA:19,0 -DA:20,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:37,0 -DA:38,0 -DA:40,0 -DA:41,0 -DA:43,0 -DA:52,0 -DA:53,0 -DA:69,0 -DA:70,0 -DA:79,0 -DA:80,0 -DA:93,0 -DA:97,0 -DA:108,0 -DA:109,0 -DA:113,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -LF:27 -LH:0 -BRDA:34,0,0,0 -BRDA:34,0,1,0 -BRDA:35,1,0,0 -BRDA:35,1,1,0 -BRDA:38,2,0,0 -BRDA:38,2,1,0 -BRDA:38,3,0,0 -BRDA:38,3,1,0 -BRDA:40,4,0,0 -BRDA:40,4,1,0 -BRDA:40,5,0,0 -BRDA:40,5,1,0 -BRDA:70,6,0,0 -BRDA:70,6,1,0 -BRDA:97,7,0,0 -BRDA:97,7,1,0 -BRF:16 -BRH:0 -end_of_record diff --git a/jest.config.ts b/jest.config.ts index 6cf798c..30993a5 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -24,21 +24,15 @@ const config: Config = { // Coverage settings collectCoverageFrom: [ - "app/*/.ts", - "app/*/.tsx", - "app/api/listings/route.ts", - "app/api/listings/[id]/route.ts", - "services/*/.ts", - "services/listings/listings.ts", - "models/*/.ts", - // "lib/**/*.ts", // Include all TypeScript files in the lib directory - // "!lib/**/*.d.ts", // Exclude type declaration files - // "!lib/__tests__/**/*.ts", // Exclude test files - "lib/*/.ts", - "!*/.d.ts", - "!*/node_modules/*", - "!*/.next/*", - "!*/coverage/*", + "services/listings/listings.ts", // Include the service file for listings + "app/api/listings/**/*.ts", // Include all route files for the listings API + "lib/**/mongoose.ts", // include the mongoose test given + "!app/api/listings/**/__tests__/**/*.ts", // Exclude test files in the listings API + "!services/listings/**/__tests__/**/*.ts", // Exclude test files in the listings service + "!**/*.d.ts", // Exclude type declaration files + "!**/node_modules/**", // Exclude node_modules + "!**/.next/**", // Exclude Next.js build files + "!**/coverage/**", // Exclude coverage files ], coveragePathIgnorePatterns: ["/node_modules/", "/.next/", "/coverage/"], diff --git a/lib/__tests__/listings.id.route.test.ts b/lib/__tests__/listings.id.route.test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/lib/__tests__/listings.route.test.ts b/lib/__tests__/listings.route.test.ts deleted file mode 100644 index edbb283..0000000 --- a/lib/__tests__/listings.route.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { GET, POST } from "@/app/api/listings/route"; -import { getFilteredListings } from "@/services/listings/listings"; -import { mock } from "node:test"; - -jest.mock("@/services/listings/listings", () => ({ - getFilteredListings: jest.fn(), -})); - -describe("GET Request Filter Tests", () => { - // make sure tests are independent - beforeEach(() => { - jest.clearAllMocks(); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - - test("successfully returns all listings with status 200", async () => { - // arrange - const mockListings = [ - { - id: "123", - itemId: "item1", - labId: "3", - quantityAvailable: 10, - status: "ACTIVE", - createdAt: new Date(), - }, - { - id: "456", - itemId: "item2", - labId: "3", - quantityAvailable: 5, - status: "ACTIVE", - createdAt: new Date(), - }, - ]; - const mockPagination = { - page: 2, - limit: 5, - total: 5, - totalPages: 1, - }; - const mockResult = { - success: true, - data: mockListings, - pagination: mockPagination, - }; - - (getFilteredListings as jest.Mock).mockResolvedValue({ - listings: mockListings, - pagination: mockPagination, - }); - - const mockRequest = new Request("/listings?labId=3&page=2&limit=5", { - method: "GET", - headers: { "Content-Type": "application/json" }, - }); - - // act - const response = await GET(mockRequest); - const responseBody = await response.json(); - - // assert - expect(responseBody).toHaveProperty("success", true); - expect(responseBody).toHaveProperty("data"); - expect(responseBody).toHaveProperty("pagination"); - expect(responseBody.data).toEqual(mockListings); - expect(responseBody.pagination.page).toEqual(2); - expect(responseBody.pagination.limit).toEqual(5); - expect(responseBody.pagination.total).toEqual(5); - expect(responseBody.pagination.totalPages).toEqual(1); - expect(responseBody).toEqual(mockResult); - - // Verify getFilteredListings was called correctly - expect(getFilteredListings).toHaveBeenCalledWith({ - labId: "3", - itemId: undefined, - page: 2, - limit: 5, - }); - }); -}); - -describe("POST Request Tests", () => { - // make sure tests are independent - beforeEach(() => { - jest.clearAllMocks(); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - - test("creates a new listing successfully", async () => { - const listingData = {}; - const response = {}; - }); - - test("handles API error during incomplete listing creation", async () => { - // arrange - const mockResult = { - success: false, - message: "Invalid request body.", - }; - const mockPostData = JSON.stringify({ labId: 1 }); // data incomplete - const mockRequest = new Request("/listings", { - method: "POST", - body: mockPostData, - headers: { "Content-Type": "application/json" }, - }); - - // act - const response = await POST(mockRequest); - const responseBody = await response.json(); - - // assert - expect(response.status).toBe(400); - expect(responseBody).toEqual(mockResult); - }); -}); diff --git a/lib/__tests__/services.listings.test.ts b/lib/__tests__/services.listings.test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/lib/mongoose.ts b/lib/mongoose.ts index f50cb5a..3a2b16f 100644 --- a/lib/mongoose.ts +++ b/lib/mongoose.ts @@ -69,4 +69,5 @@ export async function disconnectDatabase() { function test() { console.log("not tested"); // making sure coverage can see which aren't tested + // coverage also can't test things that don't occur like errors } diff --git a/models/Listing.ts b/models/Listing.ts index 585b2a4..9de3074 100644 --- a/models/Listing.ts +++ b/models/Listing.ts @@ -8,8 +8,8 @@ import { models, } from "mongoose"; -const MONGODB_URI = process.env.DATABASE_URL!; -mongoose.connect(MONGODB_URI); +// const MONGODB_URI = process.env.DATABASE_URL!; +// mongoose.connect(MONGODB_URI); const transformDocument = (_: unknown, ret: Record) => { ret.id = ret._id?.toString(); diff --git a/services/listings/__tests__/listings.test.ts b/services/listings/__tests__/listings.test.ts new file mode 100644 index 0000000..75202a4 --- /dev/null +++ b/services/listings/__tests__/listings.test.ts @@ -0,0 +1,342 @@ +import ListingModel, { ListingInput } from "@/models/Listing"; +import { + getListings, + getFilteredListings, + getListing, + addListing, + updateListing, + deleteListing, +} from "@/services/listings/listings"; + +// functions depend on model which we can't use to call db, so mock +jest.mock("@/models/Listing"); + +describe("Services: Successful Return Tests", () => { + // make sure tests are independent + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("sucessfully get all listings", async () => { + // arrange + const date = new Date(); + const id = "123"; + + const listingData = { + id: id, + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + const listingDoc = { + toObject: jest.fn().mockReturnValue(listingData), + }; + + (ListingModel.find as jest.Mock).mockReturnValue({ + exec: jest.fn().mockResolvedValue([listingDoc]), + }); + + // act + const result = await getListings(); + + // assert + expect(result.length).toBe(1); + expect(result).toEqual([listingData]); + }); + + test("successfully getFilteredListings", async () => { + // arrange + const date = new Date(); + const id = "123"; + + const listingData = { + id: id, + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + const listingDoc = { + toObject: jest.fn().mockReturnValue(listingData), + }; + + (ListingModel.find as jest.Mock).mockReturnValue({ + sort: jest.fn().mockReturnThis(), // mock how the method chaining returns same query obj + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([listingDoc]), // mock the list of documents + }); + + (ListingModel.countDocuments as jest.Mock).mockResolvedValue(1); + + // act + const result = await getFilteredListings({ + page: 1, + limit: 10, + }); + + // assert + expect(result.listings.length).toBe(1); + expect(result).toEqual({ + listings: [listingData], + pagination: { page: 1, limit: 10, total: 1, totalPages: 1 }, + }); + }); + + test("successfully get a specific listing", async () => { + const date = new Date(); + const id = "123"; + + const listingData = { + id: id, + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + const listingDoc = { + toObject: jest.fn().mockReturnValue(listingData), + }; + + // id already exists, this mock only needs toObject method + (ListingModel.findById as jest.Mock).mockReturnValue({ + exec: jest.fn().mockResolvedValue(listingDoc), // mock the mongoose doc + }); + + const result = await getListing(id); + + expect(result).toEqual(listingData); + expect(ListingModel.findById).toHaveBeenCalledWith(id); + }); + + test("successfully add a new listing", async () => { + const date = new Date(); + const id = "123"; + + const listingData: ListingInput = { + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + + const listingDoc = { + toObject: jest.fn().mockReturnValue({ + id: id, + ...listingData, + }), + }; + (ListingModel.create as jest.Mock).mockResolvedValue(listingDoc); + + const result = await addListing(listingData); + + expect(result).toEqual({ + id: id, + ...listingData, + }); + expect(ListingModel.create).toHaveBeenCalledWith(listingData); + }); + + test("successfully update a listing", async () => { + const date = new Date(); + const id = "123"; + + const listingData: ListingInput = { + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + + const listingDoc = { + toObject: jest.fn().mockReturnValue({ + id: id, + ...listingData, + }), + }; + (ListingModel.findByIdAndUpdate as jest.Mock).mockReturnValue({ + exec: jest.fn().mockResolvedValue(listingDoc), + }); + + const result = await updateListing(id, listingData); + + expect(result).toEqual({ + id: id, + ...listingData, + }); + expect(ListingModel.findByIdAndUpdate).toHaveBeenCalledWith( + id, + listingData, + { + new: true, + runValidators: true, + } + ); + }); + + test("successfully delete a listing", async () => { + const id = "123"; + (ListingModel.findByIdAndDelete as jest.Mock).mockReturnValue({ + exec: jest.fn().mockResolvedValue(true), + }); + + const result = await deleteListing(id); + + expect(result).toBe(true); + expect(ListingModel.findByIdAndDelete).toHaveBeenCalledWith(id); + }); +}); + +describe("Services: Null Return Tests", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("listing DNE so getListing returns null", async () => { + const id = "123"; + + const listingDoc = null; + (ListingModel.findById as jest.Mock).mockReturnValue({ + exec: jest.fn().mockResolvedValue(listingDoc), + }); + + const result = await getListing(id); + + expect(result).toBe(null); + expect(ListingModel.findById).toHaveBeenCalledWith(id); + }); + + test("listing DNE so updateListing returns null", async () => { + const date = new Date(); + const id = "123"; + + const listingData: ListingInput = { + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + + const listingDoc = null; + (ListingModel.findByIdAndUpdate as jest.Mock).mockReturnValue({ + exec: jest.fn().mockResolvedValue(listingDoc), + }); + + const result = await updateListing(id, listingData); + + expect(result).toBe(null); + expect(ListingModel.findByIdAndUpdate).toHaveBeenCalledWith( + id, + listingData, + { + new: true, + runValidators: true, + } + ); + }); +}); + +describe("Services: Error Return Tests", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("db error in getListings", async () => { + (ListingModel.find as jest.Mock).mockReturnValue({ + exec: jest.fn().mockRejectedValue(new Error("DB Error")), + }); + + await expect(getListings()).rejects.toThrow("DB Error"); + }); + + test("db error in getFilteredListings", async () => { + (ListingModel.find as jest.Mock).mockReturnValue({ + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + exec: jest.fn().mockRejectedValue(new Error("DB Error")), + }); + + await expect(getFilteredListings({ page: 1, limit: 10 })).rejects.toThrow( + "DB Error" + ); + expect(ListingModel.find).toHaveBeenCalledWith({}); + }); + + test("db error in getListing", async () => { + const id = "123"; + (ListingModel.findById as jest.Mock).mockReturnValue({ + exec: jest.fn().mockRejectedValue(new Error("DB Error")), + }); + + await expect(getListing(id)).rejects.toThrow("DB Error"); + expect(ListingModel.findById).toHaveBeenCalledWith(id); + }); + + test("db error in addListing", async () => { + const date = new Date(); + const listingData: ListingInput = { + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + + (ListingModel.create as jest.Mock).mockRejectedValue(new Error("DB Error")); + + await expect(addListing(listingData)).rejects.toThrow("DB Error"); + expect(ListingModel.create).toHaveBeenCalledWith(listingData); + }); + + test("db error in updateListing", async () => { + const date = new Date(); + const id = "123"; + const listingData: ListingInput = { + itemId: "item1", + labId: "lab1", + quantityAvailable: 5, + status: "ACTIVE", + createdAt: date, + }; + (ListingModel.findByIdAndUpdate as jest.Mock).mockReturnValue({ + exec: jest.fn().mockRejectedValue(new Error("DB Error")), + }); + + await expect(updateListing(id, listingData)).rejects.toThrow("DB Error"); + expect(ListingModel.findByIdAndUpdate).toHaveBeenCalledWith( + id, + listingData, + { + new: true, + runValidators: true, + } + ); + }); + + test("db error in deleteListing", async () => { + const id = "123"; + (ListingModel.findByIdAndDelete as jest.Mock).mockReturnValue({ + exec: jest.fn().mockRejectedValue(new Error("DB Error")), + }); + + await expect(deleteListing(id)).rejects.toThrow("DB Error"); + expect(ListingModel.findByIdAndDelete).toHaveBeenCalledWith(id); + }); +}); diff --git a/services/listings/listings.ts b/services/listings/listings.ts index 9f36e34..20f6e7d 100644 --- a/services/listings/listings.ts +++ b/services/listings/listings.ts @@ -38,7 +38,7 @@ async function getFilteredListings({ const validPage = isNaN(page) || page < 1 ? 1 : page; const validLimit = isNaN(limit) || limit < 1 ? 10 : Math.min(limit, MAX_LIMIT); - const skip = (page - 1) * validLimit; + const skip = (validPage - 1) * validLimit; const [listings, total] = await Promise.all([ ListingModel.find(query) @@ -110,6 +110,7 @@ async function deleteListing(id: string): Promise { } export { + getListings, getFilteredListings, getListing, addListing, From b460431c32dc8fe2ebcd031172f32f3e5aa4ab9b Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Tue, 7 Apr 2026 08:14:38 -0700 Subject: [PATCH 10/20] Updating jest config file to match main branch --- jest.config.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 30993a5..15c5c16 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -24,15 +24,15 @@ const config: Config = { // Coverage settings collectCoverageFrom: [ - "services/listings/listings.ts", // Include the service file for listings - "app/api/listings/**/*.ts", // Include all route files for the listings API - "lib/**/mongoose.ts", // include the mongoose test given - "!app/api/listings/**/__tests__/**/*.ts", // Exclude test files in the listings API - "!services/listings/**/__tests__/**/*.ts", // Exclude test files in the listings service - "!**/*.d.ts", // Exclude type declaration files - "!**/node_modules/**", // Exclude node_modules - "!**/.next/**", // Exclude Next.js build files - "!**/coverage/**", // Exclude coverage files + "app/*/.ts", + "app/*/.tsx", + "services/*/.ts", + "models/*/.ts", + "lib/*/.ts", + "!*/.d.ts", + "!*/node_modules/*", + "!*/.next/*", + "!*/coverage/*", ], coveragePathIgnorePatterns: ["/node_modules/", "/.next/", "/coverage/"], From fa0bc768039d9c335fee191e882995878764114d Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Sat, 11 Apr 2026 16:17:49 -0700 Subject: [PATCH 11/20] Updating listing schema and validation schema to follow front end updates --- app/api/listings/[id]/route.ts | 19 ++++++++++++++++--- app/api/listings/route.ts | 26 +++++++++++++++++++++----- models/Listing.ts | 26 +++++++++++++++++++++----- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 83fb400..f295041 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -12,10 +12,23 @@ const objectIdSchema = z .string() .regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId"); const listingValidationSchema = z.object({ - itemId: z.string().min(1), - labId: z.string().min(1), - quantityAvailable: z.number().min(1), + // handle defaults here for the optional fields + itemName: z.string(), + itemId: z.string(), + labName: z.string().optional().default(""), + labLocation: z.string().optional().default(""), + labId: z.string(), + imageUrls: z.array(z.string()).optional().default([]), + quantityAvailable: z.number(), + expiryDate: z.date().optional(), + description: z.string().optional().default(""), + price: z.number().optional().default(0), status: z.enum(["ACTIVE", "INACTIVE"]), + condition: z.enum(["New", "Good", "Fair", "Poor"]), + hazardTags: z + .array(z.enum(["Physical", "Chemical", "Biological", "Other"])) + .optional() + .default([]), }); /** diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 823cf96..803882a 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -2,13 +2,26 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; import { z } from "zod"; import { getFilteredListings, addListing } from "@/services/listings/listings"; +import { ListingInput } from "@/models/Listing"; -/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ const listingValidationSchema = z.object({ - itemId: z.string().min(1), - labId: z.string().min(1), - quantityAvailable: z.number().min(1), + // handle defaults here for the optional fields + itemName: z.string(), + itemId: z.string(), + labName: z.string().optional().default(""), + labLocation: z.string().optional().default(""), + labId: z.string(), + imageUrls: z.array(z.string()).optional().default([]), + quantityAvailable: z.number(), + expiryDate: z.date().optional(), + description: z.string().optional().default(""), + price: z.number().optional().default(0), status: z.enum(["ACTIVE", "INACTIVE"]), + condition: z.enum(["New", "Good", "Fair", "Poor"]), + hazardTags: z + .array(z.enum(["Physical", "Chemical", "Biological", "Other"])) + .optional() + .default([]), }); /** @@ -88,7 +101,10 @@ async function POST(request: Request) { } try { - const listingData = { ...parsedBody.data, createdAt: new Date() }; + const listingData = { + ...parsedBody.data, + createdAt: new Date(), + } as ListingInput; const listing = await addListing(listingData); return NextResponse.json( { diff --git a/models/Listing.ts b/models/Listing.ts index 9de3074..bf60c00 100644 --- a/models/Listing.ts +++ b/models/Listing.ts @@ -1,4 +1,3 @@ -import mongoose from "mongoose"; import { HydratedDocument, InferSchemaType, @@ -8,9 +7,6 @@ import { models, } from "mongoose"; -// const MONGODB_URI = process.env.DATABASE_URL!; -// mongoose.connect(MONGODB_URI); - const transformDocument = (_: unknown, ret: Record) => { ret.id = ret._id?.toString(); delete ret._id; @@ -19,11 +15,29 @@ const transformDocument = (_: unknown, ret: Record) => { const listingSchema = new Schema( { + itemName: { type: String, required: true }, itemId: { type: String, required: true }, + labName: { type: String }, + labLocation: { type: String }, labId: { type: String, required: true }, + imageUrls: [{ type: String }], quantityAvailable: { type: Number, required: true }, - status: { type: String, enum: ["ACTIVE", "INACTIVE"], required: true }, createdAt: { type: Date, required: true }, + expiryDate: { type: Date }, + description: { type: String, default: "" }, + price: { type: Number, default: 0 }, + status: { type: String, enum: ["ACTIVE", "INACTIVE"], required: true }, + condition: { + type: String, + enum: ["New", "Good", "Fair", "Poor"], + required: true, + }, + hazardTags: [ + { + type: String, + enum: ["Physical", "Chemical", "Biological", "Other"], + }, + ], }, { toJSON: { virtuals: true, versionKey: false, transform: transformDocument }, @@ -38,6 +52,8 @@ const listingSchema = new Schema( // for filtering listingSchema.index({ labId: 1, createdAt: -1 }); listingSchema.index({ itemId: 1, createdAt: -1 }); +listingSchema.index({ expiryDate: 1 }); +listingSchema.index({ hazardTags: 1 }); export type ListingInput = InferSchemaType; export type Listing = ListingInput & { id: string }; From 2b755130373ce493e3cbd5e82b5d35d9d9899e81 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Tue, 14 Apr 2026 21:36:56 -0700 Subject: [PATCH 12/20] Created GCS utility function for uploading image to free storage bucket and updated POST handler to allow multipart/form-data --- app/api/listings/route.ts | 46 +- lib/googleCloud.ts | 43 ++ package-lock.json | 1210 +++++++++++++++++++++++++++++++------ package.json | 1 + 4 files changed, 1121 insertions(+), 179 deletions(-) create mode 100644 lib/googleCloud.ts diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 803882a..6f9c8eb 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; -import { z } from "zod"; +import { number, z } from "zod"; import { getFilteredListings, addListing } from "@/services/listings/listings"; import { ListingInput } from "@/models/Listing"; +import { uploadImage } from "@/lib/googleCloud"; const listingValidationSchema = z.object({ // handle defaults here for the optional fields @@ -72,7 +73,7 @@ async function GET(request: Request) { /** * Create a new listing to store in db - * @param request the request with JSON data in req body + * @param request the request with multipart/form-data data in req body * @returns JSON response with success message and req body echoed */ async function POST(request: Request) { @@ -84,11 +85,44 @@ async function POST(request: Request) { { status: 500 } ); } + const formData = await request.formData(); + const entries = Array.from(formData.entries()); - // assuming frontend sends req with content-type set to app/json - // content type automatically set as app/json - const body = await request.json(); - const parsedBody = listingValidationSchema.safeParse(body); + // separate image and hazardTags from other fields + const textEntries: [string, FormDataEntryValue][] = []; + const hazardTags: string[] = []; + + for (const [key, value] of entries) { + if (key === "image") continue; + if (key === "hazardTags") { + hazardTags.push(value as string); + } else { + textEntries.push([key, value]); + } + } + + const result = Object.fromEntries(textEntries) as Partial; + result.hazardTags = hazardTags as typeof result.hazardTags; + + // convert types (since formData changed to string) + result.quantityAvailable = Number(result.quantityAvailable); + result.price = Number(result.price); + if (result.expiryDate) { + result.expiryDate = new Date(result.expiryDate as unknown as string); + } + + const imageFiles = formData.getAll("images") as File[]; + 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); + } + result.imageUrls = imageUrls; + } + + const parsedBody = listingValidationSchema.safeParse(result); if (!parsedBody.success) { return NextResponse.json( diff --git a/lib/googleCloud.ts b/lib/googleCloud.ts new file mode 100644 index 0000000..8f2a3cd --- /dev/null +++ b/lib/googleCloud.ts @@ -0,0 +1,43 @@ +import { Storage } from "@google-cloud/storage"; + +/** + * create the authenticated client for interacting with GCS + */ +const storage = new Storage({ + projectId: process.env.GOOGLE_CLOUD_PROJECT_ID, + credentials: { + client_email: process.env.GOOGLE_CLOUD_CLIENT_EMAIL, + private_key: process.env.GOOGLE_CLOUD_PRIVATE_KEY?.replace(/\\n/g, "\n"), + }, +}); + +const bucketName = process.env.GOOGLE_CLOUD_BUCKET_NAME!; +const bucket = storage.bucket(bucketName); + +/** + * Upload a publicly accessible image file to GCS + * @param file image as raw bytes of data + * @param originalFilename original name of the image file + * @returns a promise that resolves to the public URL of the uploaded image + */ +export async function uploadImage( + file: Buffer, + originalFilename: string +): Promise { + const timestamp = Date.now(); + const safeFilename = originalFilename.replace(/\s/g, "_"); // replace spaces + const uniqueFilename = `listings/${timestamp}-${safeFilename}`; // unique by timestamp + + const blob = bucket.file(uniqueFilename); + + await blob.save(file, { + metadata: { + contentType: "image/jpeg", + }, + }); + + // Make the file publicly accessible + await blob.makePublic(); + + return `https://storage.googleapis.com/${bucketName}/${uniqueFilename}`; +} diff --git a/package-lock.json b/package-lock.json index 5f42d16..7a4b5a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "nextjs", "version": "0.1.0", "dependencies": { + "@google-cloud/storage": "^7.19.0", "@playwright/test": "^1.49.0", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.1", @@ -754,20 +755,22 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -784,6 +787,75 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -800,20 +872,22 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1937,9 +2011,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1980,9 +2054,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -1996,9 +2070,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -2012,9 +2086,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -2028,9 +2102,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -2044,9 +2118,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -2060,9 +2134,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -2076,9 +2150,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -2092,9 +2166,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -2120,6 +2194,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-1.1.0.tgz", + "integrity": "sha512-bidpxmTBP0pOsxULw6XlxzQpTgrAGLDHGBK/JuWhPDL6ZV0GZ/PmN9CA9do6e+A9lYI6qx6ikJUtJYRxup141g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2387,6 +2473,15 @@ } } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -2479,6 +2574,12 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -2576,7 +2677,6 @@ "version": "20.17.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -2606,6 +2706,35 @@ "@types/react": "*" } }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2637,6 +2766,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -3145,6 +3280,18 @@ "win32" ] }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3183,17 +3330,17 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3448,6 +3595,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3471,11 +3627,19 @@ "tslib": "^2.4.0" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/available-typed-arrays": { @@ -3631,6 +3795,26 @@ } } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -3643,6 +3827,15 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3656,10 +3849,11 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3751,6 +3945,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3781,7 +3981,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4060,7 +4259,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4177,6 +4375,18 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4314,7 +4524,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4408,7 +4617,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4419,12 +4627,33 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", @@ -4451,6 +4680,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -4538,7 +4776,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4548,7 +4785,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -4583,7 +4819,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4596,7 +4831,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4866,10 +5100,11 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4897,10 +5132,11 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4947,20 +5183,22 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5013,10 +5251,11 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5035,10 +5274,11 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5101,20 +5341,22 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5195,6 +5437,15 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -5264,6 +5515,12 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5324,6 +5581,42 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.6.0.tgz", + "integrity": "sha512-5G+uaEBbOm9M4dgMOV3K/rBzfUNGqGqoUTaYJM3hBwM8t71w07gxLQZoTsjkY8FtfjabqgQHEkeIySBDYeBmJw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^1.1.0", + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -5343,6 +5636,32 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5442,15 +5761,16 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -5510,6 +5830,21 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -5551,7 +5886,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5583,6 +5917,96 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5607,7 +6031,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5642,7 +6065,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5728,20 +6150,22 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5780,11 +6204,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5805,10 +6268,23 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5873,7 +6349,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5886,7 +6361,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -5901,26 +6375,66 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "debug": "4" }, "engines": { - "node": ">= 0.4" + "node": ">= 6.0.0" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -6033,8 +6547,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.7", @@ -6365,7 +6878,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7419,9 +7931,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7575,10 +8087,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7599,6 +8112,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7651,6 +8173,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -7826,7 +8369,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7893,7 +8435,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7903,7 +8444,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -7933,12 +8473,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -8091,6 +8632,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mongodb-memory-server-core/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/mongodb-memory-server-core/node_modules/mongodb": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz", @@ -8152,6 +8739,27 @@ "node": ">=20.19.0" } }, + "node_modules/mongodb-memory-server-core/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/mongoose": { "version": "8.19.4", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.4.tgz", @@ -8209,15 +8817,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8268,14 +8877,14 @@ } }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -8287,15 +8896,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -8374,6 +8983,71 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8543,7 +9217,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -8585,7 +9258,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -8667,6 +9339,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8720,10 +9407,11 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -9202,6 +9890,20 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9335,6 +10037,29 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -9402,6 +10127,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -9706,6 +10451,21 @@ "node": ">=8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/streamx": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", @@ -9718,6 +10478,15 @@ "text-decoder": "^1.1.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -9965,6 +10734,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -10010,10 +10797,12 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -10196,6 +10985,60 @@ } } }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -10212,9 +11055,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -10223,9 +11066,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10636,8 +11479,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unrs-resolver": { "version": "1.11.1", @@ -10717,8 +11559,16 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", @@ -10752,6 +11602,18 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10981,8 +11843,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "5.0.1", @@ -11016,15 +11877,19 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { @@ -11079,9 +11944,9 @@ } }, "node_modules/yauzl": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", - "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11106,7 +11971,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 88b34fe..3ac20e4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:ci": "jest --coverage --ci --forceExit" }, "dependencies": { + "@google-cloud/storage": "^7.19.0", "@playwright/test": "^1.49.0", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.1", From e7b049648c7a82d202dba348267826f45be2d430 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Wed, 15 Apr 2026 20:06:52 -0700 Subject: [PATCH 13/20] Updating tests and addressing ci/cd test fails --- app/api/listings/__tests__/route.test.ts | 28 ++++++- services/listings/__tests__/listings.test.ts | 88 ++++++++++++++++++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/app/api/listings/__tests__/route.test.ts b/app/api/listings/__tests__/route.test.ts index 8a82164..8bbca94 100644 --- a/app/api/listings/__tests__/route.test.ts +++ b/app/api/listings/__tests__/route.test.ts @@ -17,6 +17,10 @@ jest.mock("@/services/listings/listings", () => ({ deleteListing: jest.fn(), })); +jest.mock("@/lib/googleCloud", () => ({ + uploadImage: jest.fn().mockResolvedValue("https://mock.com/image.jpg"), +})); + /** import after mocking */ import { getListings, @@ -42,12 +46,21 @@ describe("API: Successful Responses", () => { const id = "123"; const listingData = { - id: id, // just keep as string now since res.json() stringifies + id: id, + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; (connectToDatabase as jest.Mock).mockResolvedValue({}); @@ -87,11 +100,20 @@ describe("API: Successful Responses", () => { const listingData = { id: id, + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; (connectToDatabase as jest.Mock).mockResolvedValue({}); diff --git a/services/listings/__tests__/listings.test.ts b/services/listings/__tests__/listings.test.ts index 75202a4..08ccecd 100644 --- a/services/listings/__tests__/listings.test.ts +++ b/services/listings/__tests__/listings.test.ts @@ -27,11 +27,20 @@ describe("Services: Successful Return Tests", () => { const listingData = { id: id, + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; const listingDoc = { toObject: jest.fn().mockReturnValue(listingData), @@ -56,11 +65,20 @@ describe("Services: Successful Return Tests", () => { const listingData = { id: id, + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; const listingDoc = { toObject: jest.fn().mockReturnValue(listingData), @@ -95,11 +113,20 @@ describe("Services: Successful Return Tests", () => { const listingData = { id: id, + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; const listingDoc = { toObject: jest.fn().mockReturnValue(listingData), @@ -121,11 +148,20 @@ describe("Services: Successful Return Tests", () => { const id = "123"; const listingData: ListingInput = { + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; const listingDoc = { @@ -150,11 +186,20 @@ describe("Services: Successful Return Tests", () => { const id = "123"; const listingData: ListingInput = { + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; const listingDoc = { @@ -223,11 +268,20 @@ describe("Services: Null Return Tests", () => { const id = "123"; const listingData: ListingInput = { + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; const listingDoc = null; @@ -292,11 +346,20 @@ describe("Services: Error Return Tests", () => { test("db error in addListing", async () => { const date = new Date(); const listingData: ListingInput = { + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; (ListingModel.create as jest.Mock).mockRejectedValue(new Error("DB Error")); @@ -309,11 +372,20 @@ describe("Services: Error Return Tests", () => { const date = new Date(); const id = "123"; const listingData: ListingInput = { + itemName: "Flask", itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", labId: "lab1", + imageUrls: [], quantityAvailable: 5, - status: "ACTIVE", createdAt: date, + expiryDate: null, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], }; (ListingModel.findByIdAndUpdate as jest.Mock).mockReturnValue({ exec: jest.fn().mockRejectedValue(new Error("DB Error")), From e7aee1df3b23325243ac46445fedaa29b9930d41 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Wed, 15 Apr 2026 22:34:26 -0700 Subject: [PATCH 14/20] Updating PUT handler to receive formData and adding more test cases to API --- app/api/listings/[id]/route.ts | 64 ++++++++++++- app/api/listings/__tests__/route.test.ts | 116 ++++++++++++++++++++++- app/api/listings/route.ts | 1 + 3 files changed, 177 insertions(+), 4 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index f295041..186f5e4 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -6,6 +6,8 @@ import { getListing, updateListing, } from "@/services/listings/listings"; +import { ListingInput } from "@/models/Listing"; +import { uploadImage } from "@/lib/googleCloud"; /* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ const objectIdSchema = z @@ -92,6 +94,10 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { const parsedId = objectIdSchema.safeParse(params.id); if (!parsedId.success) { + console.log( + "PUT - Validation error:", + JSON.stringify(parsedId.error?.issues, null, 2) + ); return NextResponse.json( { success: false, @@ -101,7 +107,60 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { ); } - const body = await request.json(); + const formData = await request.formData(); + const entries = Array.from(formData.entries()); + + // separate image and hazardTags from other fields + const textEntries: [string, FormDataEntryValue][] = []; + const hazardTags: string[] = []; + + for (const [key, value] of entries) { + if (key === "images") continue; + if (key === "hazardTags") { + hazardTags.push(value as string); + } else { + textEntries.push([key, value]); + } + } + + // create plain JS object + const result = Object.fromEntries(textEntries) as Partial; + result.hazardTags = hazardTags as typeof result.hazardTags; + + // convert types (since formData changed to string) + if (result.quantityAvailable) { + result.quantityAvailable = Number(result.quantityAvailable); + } + if (result.price) { + result.price = Number(result.price); + } + if (result.expiryDate) { + result.expiryDate = new Date(result.expiryDate as unknown as string); + } + + // handle new image uploads + const imageFiles = formData.getAll("images") as File[]; + const existingImageUrls = formData.get("existingImageUrls"); + + let allImageUrls: string[] = []; + + // parse existing image URLs if provided + if (existingImageUrls) { + try { + allImageUrls = JSON.parse(existingImageUrls as string); + } catch { + // invalid JSON + allImageUrls = []; + } + } + + // upload new images and add to array + for (const imageFile of imageFiles) { + const buffer = Buffer.from(await imageFile.arrayBuffer()); + const imageUrl = await uploadImage(buffer, imageFile.name); + allImageUrls.push(imageUrl); + } + result.imageUrls = allImageUrls; const validator = z.object({ id: objectIdSchema, @@ -110,8 +169,9 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { const parsedRequest = validator.safeParse({ id: parsedId.data, - update: body, + update: result, }); + if (!parsedRequest.success) { return NextResponse.json( { diff --git a/app/api/listings/__tests__/route.test.ts b/app/api/listings/__tests__/route.test.ts index 8bbca94..32a040a 100644 --- a/app/api/listings/__tests__/route.test.ts +++ b/app/api/listings/__tests__/route.test.ts @@ -1,4 +1,5 @@ import mongoose from "mongoose"; +import ListingModel, { ListingInput } from "@/models/Listing"; import { GET, POST } from "@/app/api/listings/route"; import { GET as GET_BY_ID, PUT, DELETE } from "@/app/api/listings/[id]/route"; import { connectToDatabase } from "@/lib/mongoose"; @@ -126,15 +127,126 @@ describe("API: Successful Responses", () => { expect(res.status).toEqual(200); expect(body.success).toEqual(true); expect(body.data).toEqual(listingData); + expect(getListing).toHaveBeenCalledWith(id); }); }); describe("POST /listings", () => { - test("creates a new listing successfully", async () => {}); + test("creates a new listing successfully", async () => { + const date = new Date(); + + const listingData = { + itemName: "Flask", + itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", + labId: "lab1", + quantityAvailable: 5, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], + }; + + const mockReturnedData = { + ...listingData, + id: "123", + createdAt: date, + imageUrls: [], + }; + + const formData = new FormData(); + Object.entries(listingData).forEach(([key, value]) => { + if (value === null || value === undefined) return; + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, String(value)); + } + }); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (addListing as jest.Mock).mockResolvedValue(mockReturnedData); + + const req = new Request(`http://localhost/api/listings`, { + method: "POST", + body: formData, + }); + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toEqual(201); + expect(body.success).toEqual(true); + expect(body.data).toMatchObject({ + ...listingData, + id: "123", + createdAt: expect.any(String), + imageUrls: [], + }); + expect(addListing).toHaveBeenCalled(); + }); }); describe("PUT /listings/[id]", () => { - test("updates a listing successfully", async () => {}); + test("updates a listing successfully", async () => { + const date = new Date(); + const id = new mongoose.Types.ObjectId().toString(); + + const listingData = { + itemName: "Flask", + itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", + labId: "lab1", + quantityAvailable: 5, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical"], + }; + + const mockReturnedData = { + ...listingData, + id: id, + createdAt: date, + imageUrls: [], + }; + + const formData = new FormData(); + Object.entries(listingData).forEach(([key, value]) => { + if (value === null || value === undefined) return; + if (Array.isArray(value)) { + value.forEach((item) => formData.append(key, item)); + } else { + formData.append(key, String(value)); + } + }); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (updateListing as jest.Mock).mockResolvedValue(mockReturnedData); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "PUT", + body: formData, + }); + const res = await PUT(req, { params: { id: id } }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.success).toEqual(true); + expect(body.data).toMatchObject({ + ...listingData, + id: id, + createdAt: expect.any(String), + imageUrls: [], + }); + expect(updateListing).toHaveBeenCalledWith( + id, + expect.objectContaining(listingData) + ); + }); }); describe("DELETE /listings/[id]", () => { diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 6f9c8eb..33f8bd3 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -101,6 +101,7 @@ async function POST(request: Request) { } } + // create plain JS object const result = Object.fromEntries(textEntries) as Partial; result.hazardTags = hazardTags as typeof result.hazardTags; From 6864f7025a7ecab44ad9f517512a919e050a8073 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Sat, 18 Apr 2026 17:54:16 -0700 Subject: [PATCH 15/20] Updated DELETE handler to return 200 to maintain a JSON response (204 doesn't allow) --- app/api/listings/[id]/route.ts | 3 +- app/api/listings/__tests__/route.test.ts | 91 ++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 186f5e4..93cdc74 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -234,6 +234,7 @@ async function DELETE( } const parsedId = objectIdSchema.safeParse(params.id); + console.log("DELETE - Parsed ID:", parsedId); if (!parsedId.success) { return NextResponse.json( { @@ -260,7 +261,7 @@ async function DELETE( success: true, message: "Listing successfully deleted.", }, - { status: 204 } + { status: 200 } ); } catch { return NextResponse.json( diff --git a/app/api/listings/__tests__/route.test.ts b/app/api/listings/__tests__/route.test.ts index 32a040a..4779174 100644 --- a/app/api/listings/__tests__/route.test.ts +++ b/app/api/listings/__tests__/route.test.ts @@ -250,7 +250,23 @@ describe("API: Successful Responses", () => { }); describe("DELETE /listings/[id]", () => { - test("deletes a listing successfully", async () => {}); + test("deletes a listing successfully", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (deleteListing as jest.Mock).mockResolvedValue(true); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "DELETE", + }); + const res = await DELETE(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(200); + expect(body.success).toBe(true); + expect(body.message).toBe("Listing successfully deleted."); + expect(deleteListing).toHaveBeenCalledWith(id); + }); }); }); @@ -377,9 +393,74 @@ describe("API: Error Responses", () => { }); describe("DELETE /listings/[id]", () => { - test("DB connection error", async () => {}); - test("invalid id format", async () => {}); - test("listing not found", async () => {}); - test("service error deleting listing", async () => {}); + test("DB connection error", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "DELETE", + }); + const res = await DELETE(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toBe(false); + expect(body.message).toBe("Error connecting to database."); + }); + + test("invalid id format", async () => { + const id = "invalid-id"; + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "DELETE", + }); + const res = await DELETE(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.success).toBe(false); + expect(body.message).toBe( + "Invalid ID format. Must be a valid MongoDB ObjectId." + ); + }); + + test("listing not found", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (deleteListing as jest.Mock).mockResolvedValue(false); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "DELETE", + }); + const res = await DELETE(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(404); + expect(body.success).toBe(false); + expect(body.message).toBe("Listing not found"); + expect(deleteListing).toHaveBeenCalledWith(id); + }); + + test("service error deleting listing", async () => { + const id = new mongoose.Types.ObjectId().toString(); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (deleteListing as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "DELETE", + }); + const res = await DELETE(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toBe(false); + expect(body.message).toBe("Error occurred while deleting listing."); + expect(deleteListing).toHaveBeenCalledWith(id); + }); }); }); From ea30d5de8847529a14d2643b6eec05f0db1dd8c4 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Sun, 19 Apr 2026 11:43:55 -0700 Subject: [PATCH 16/20] Finishing all test cases for API endpoints --- app/api/listings/[id]/route.ts | 127 ++++---- app/api/listings/__tests__/route.test.ts | 360 ++++++++++++++++++++--- app/api/listings/route.ts | 6 +- 3 files changed, 374 insertions(+), 119 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 93cdc74..2fd9863 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -9,29 +9,29 @@ import { import { ListingInput } from "@/models/Listing"; import { uploadImage } from "@/lib/googleCloud"; -/* IMPORTANT: implement user auth in future (e.g. only lab admins create/delete) */ const objectIdSchema = z .string() .regex(/^[0-9a-fA-F]{24}$/, "Invalid MongoDB ObjectId"); -const listingValidationSchema = z.object({ - // handle defaults here for the optional fields - itemName: z.string(), - itemId: z.string(), - labName: z.string().optional().default(""), - labLocation: z.string().optional().default(""), - labId: z.string(), - imageUrls: z.array(z.string()).optional().default([]), - quantityAvailable: z.number(), - expiryDate: z.date().optional(), - description: z.string().optional().default(""), - price: z.number().optional().default(0), - status: z.enum(["ACTIVE", "INACTIVE"]), - condition: z.enum(["New", "Good", "Fair", "Poor"]), - hazardTags: z - .array(z.enum(["Physical", "Chemical", "Biological", "Other"])) - .optional() - .default([]), -}); +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 @@ -94,10 +94,6 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { const parsedId = objectIdSchema.safeParse(params.id); if (!parsedId.success) { - console.log( - "PUT - Validation error:", - JSON.stringify(parsedId.error?.issues, null, 2) - ); return NextResponse.json( { success: false, @@ -108,70 +104,46 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { } const formData = await request.formData(); - const entries = Array.from(formData.entries()); - - // separate image and hazardTags from other fields - const textEntries: [string, FormDataEntryValue][] = []; - const hazardTags: string[] = []; + const updateData: Partial = { + ...Object.fromEntries(formData.entries()), + }; - for (const [key, value] of entries) { - if (key === "images") continue; - if (key === "hazardTags") { - hazardTags.push(value as string); - } else { - textEntries.push([key, value]); - } + // handle array fields + const hazardTags = formData.getAll("hazardTags"); + if (hazardTags.length > 0) { + updateData.hazardTags = hazardTags as ListingInput["hazardTags"]; } - // create plain JS object - const result = Object.fromEntries(textEntries) as Partial; - result.hazardTags = hazardTags as typeof result.hazardTags; - - // convert types (since formData changed to string) - if (result.quantityAvailable) { - result.quantityAvailable = Number(result.quantityAvailable); + // type conversions + if (updateData.quantityAvailable !== undefined) { + updateData.quantityAvailable = Number(updateData.quantityAvailable); } - if (result.price) { - result.price = Number(result.price); + + if (updateData.price !== undefined) { + updateData.price = Number(updateData.price); } - if (result.expiryDate) { - result.expiryDate = new Date(result.expiryDate as unknown as string); + + if (updateData.expiryDate !== undefined) { + updateData.expiryDate = new Date( + updateData.expiryDate as unknown as string + ); } - // handle new image uploads + // handle image uploads if provided const imageFiles = formData.getAll("images") as File[]; - const existingImageUrls = formData.get("existingImageUrls"); + if (imageFiles.length > 0) { + const imageUrls: string[] = []; - let allImageUrls: string[] = []; - - // parse existing image URLs if provided - if (existingImageUrls) { - try { - allImageUrls = JSON.parse(existingImageUrls as string); - } catch { - // invalid JSON - allImageUrls = []; + for (const imageFile of imageFiles) { + const buffer = Buffer.from(await imageFile.arrayBuffer()); + const imageUrl = await uploadImage(buffer, imageFile.name); + imageUrls.push(imageUrl); } - } - // upload new images and add to array - for (const imageFile of imageFiles) { - const buffer = Buffer.from(await imageFile.arrayBuffer()); - const imageUrl = await uploadImage(buffer, imageFile.name); - allImageUrls.push(imageUrl); + updateData.imageUrls = imageUrls; } - result.imageUrls = allImageUrls; - - const validator = z.object({ - id: objectIdSchema, - update: listingValidationSchema.partial(), - }); - - const parsedRequest = validator.safeParse({ - id: parsedId.data, - update: result, - }); + const parsedRequest = listingValidationSchema.safeParse(updateData); if (!parsedRequest.success) { return NextResponse.json( { @@ -185,8 +157,9 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { try { const updatedListing = await updateListing( parsedId.data, - parsedRequest.data.update + parsedRequest.data ); + if (!updatedListing) { return NextResponse.json( { @@ -196,6 +169,7 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { { status: 404 } ); } + return NextResponse.json( { success: true, @@ -234,7 +208,6 @@ async function DELETE( } const parsedId = objectIdSchema.safeParse(params.id); - console.log("DELETE - Parsed ID:", parsedId); if (!parsedId.success) { return NextResponse.json( { diff --git a/app/api/listings/__tests__/route.test.ts b/app/api/listings/__tests__/route.test.ts index 4779174..08c2ac5 100644 --- a/app/api/listings/__tests__/route.test.ts +++ b/app/api/listings/__tests__/route.test.ts @@ -1,5 +1,4 @@ import mongoose from "mongoose"; -import ListingModel, { ListingInput } from "@/models/Listing"; import { GET, POST } from "@/app/api/listings/route"; import { GET as GET_BY_ID, PUT, DELETE } from "@/app/api/listings/[id]/route"; import { connectToDatabase } from "@/lib/mongoose"; @@ -24,7 +23,6 @@ jest.mock("@/lib/googleCloud", () => ({ /** import after mocking */ import { - getListings, getFilteredListings, getListing, addListing, @@ -132,7 +130,7 @@ describe("API: Successful Responses", () => { }); describe("POST /listings", () => { - test("creates a new listing successfully", async () => { + test("creates a new listing with all fields", async () => { const date = new Date(); const listingData = { @@ -146,11 +144,13 @@ describe("API: Successful Responses", () => { price: 50, status: "ACTIVE", condition: "New", - hazardTags: ["Physical"], + hazardTags: ["Physical", "Chemical"], + expiryDate: date.toISOString(), }; const mockReturnedData = { ...listingData, + expiryDate: new Date(listingData.expiryDate), id: "123", createdAt: date, imageUrls: [], @@ -158,7 +158,6 @@ describe("API: Successful Responses", () => { const formData = new FormData(); Object.entries(listingData).forEach(([key, value]) => { - if (value === null || value === undefined) return; if (Array.isArray(value)) { value.forEach((item) => formData.append(key, item)); } else { @@ -173,45 +172,125 @@ describe("API: Successful Responses", () => { method: "POST", body: formData, }); + const res = await POST(req); const body = await res.json(); - expect(res.status).toEqual(201); - expect(body.success).toEqual(true); + expect(res.status).toBe(201); + expect(body.success).toBe(true); + expect(body.data).toMatchObject({ - ...listingData, + itemName: "Flask", + itemId: "item1", + labName: "Dr. Jones Lab", + labLocation: "Torrey Pines", + labId: "lab1", + quantityAvailable: 5, + description: "High quality flask for use", + price: 50, + status: "ACTIVE", + condition: "New", + hazardTags: ["Physical", "Chemical"], + expiryDate: expect.any(String), id: "123", createdAt: expect.any(String), imageUrls: [], }); - expect(addListing).toHaveBeenCalled(); + + expect(addListing).toHaveBeenCalledWith( + expect.objectContaining({ + ...listingData, + quantityAvailable: 5, + price: 50, + expiryDate: new Date(listingData.expiryDate), + hazardTags: ["Physical", "Chemical"], + }) + ); }); - }); - describe("PUT /listings/[id]", () => { - test("updates a listing successfully", async () => { + test("creates listing with defaults for optional fields", async () => { const date = new Date(); - const id = new mongoose.Types.ObjectId().toString(); - const listingData = { + const minimalData = { itemName: "Flask", itemId: "item1", - labName: "Dr. Jones Lab", - labLocation: "Torrey Pines", labId: "lab1", quantityAvailable: 5, - description: "High quality flask for use", - price: 50, status: "ACTIVE", condition: "New", - hazardTags: ["Physical"], }; const mockReturnedData = { - ...listingData, - id: id, + ...minimalData, + id: "123", createdAt: date, imageUrls: [], + labName: "", + labLocation: "", + description: "", + price: 0, + hazardTags: [], + }; + + const formData = new FormData(); + Object.entries(minimalData).forEach(([key, value]) => { + formData.append(key, String(value)); + }); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (addListing as jest.Mock).mockResolvedValue(mockReturnedData); + + const req = new Request(`http://localhost/api/listings`, { + method: "POST", + body: formData, + }); + + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toBe(201); + expect(body.success).toBe(true); + + expect(body.data).toMatchObject({ + ...minimalData, + labName: "", + labLocation: "", + description: "", + price: 0, + hazardTags: [], + imageUrls: [], + }); + }); + }); + + describe("PUT /listings/[id]", () => { + test("successfully updates a listing with all fields", async () => { + const id = new mongoose.Types.ObjectId().toString(); + const date = new Date(); + + const listingData = { + itemName: "Updated Flask", + itemId: "item1", + labName: "Updated Lab", + labLocation: "Updated Location", + labId: "lab1", + quantityAvailable: 10, + description: "Updated description", + price: 100, + status: "ACTIVE", + condition: "Good", + hazardTags: ["Chemical", "Biological"], + expiryDate: date.toISOString(), + }; + + const mockReturnedData = { + ...listingData, + id, + createdAt: date, + imageUrls: [ + "http://example.com/image1.jpg", + "http://example.com/image2.jpg", + ], }; const formData = new FormData(); @@ -231,21 +310,26 @@ describe("API: Successful Responses", () => { method: "PUT", body: formData, }); - const res = await PUT(req, { params: { id: id } }); + + const res = await PUT(req, { params: { id } }); const body = await res.json(); - expect(res.status).toEqual(200); - expect(body.success).toEqual(true); - expect(body.data).toMatchObject({ + expect(res.status).toBe(200); + expect(body.success).toBe(true); + expect(body.message).toBe("Listing successfully updated."); + expect(body.data).toEqual({ ...listingData, - id: id, + id, createdAt: expect.any(String), - imageUrls: [], + imageUrls: [ + "http://example.com/image1.jpg", + "http://example.com/image2.jpg", + ], + }); + expect(updateListing).toHaveBeenCalledWith(id, { + ...listingData, + expiryDate: new Date(listingData.expiryDate), }); - expect(updateListing).toHaveBeenCalledWith( - id, - expect.objectContaining(listingData) - ); }); }); @@ -378,18 +462,214 @@ describe("API: Error Responses", () => { }); describe("POST /listings", () => { - test("DB connection error", async () => {}); - test("invalid req body", async () => {}); - test("listing already exists", async () => {}); - test("service error creating listing", async () => {}); + test("DB connection error", async () => { + (connectToDatabase as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const formData = new FormData(); + + const req = new Request("http://localhost/api/listings", { + method: "POST", + body: formData, + }); + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toBe(false); + expect(body.message).toBe("Error connecting to database."); + }); + + test("invalid req body", async () => { + (connectToDatabase as jest.Mock).mockResolvedValue({}); + + const formData = new FormData(); + formData.append("invalidField", "Invalid Value"); + + const req = new Request("http://localhost/api/listings", { + method: "POST", + body: formData, + }); + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.success).toBe(false); + expect(body.message).toBe("Invalid request body."); + }); + + test("listing already exists", async () => { + (connectToDatabase as jest.Mock).mockResolvedValue({}); + // mock mongodb's error, not the http 409 status code + (addListing as jest.Mock).mockRejectedValue({ code: 11000 }); + + const formData = new FormData(); + formData.append("itemName", "Flask"); + formData.append("itemId", "item1"); + formData.append("labId", "lab1"); + formData.append("quantityAvailable", "5"); + formData.append("status", "ACTIVE"); + formData.append("condition", "New"); + + const req = new Request("http://localhost/api/listings", { + method: "POST", + body: formData, + }); + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toBe(409); + expect(body.success).toBe(false); + expect(body.message).toBe("This listing already exists."); + expect(addListing).toHaveBeenCalled(); + }); + + test("service error creating listing", async () => { + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (addListing as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const formData = new FormData(); + formData.append("itemName", "Flask"); + formData.append("itemId", "item1"); + formData.append("labId", "lab1"); + formData.append("quantityAvailable", "5"); + formData.append("status", "ACTIVE"); + formData.append("condition", "New"); + + const req = new Request("http://localhost/api/listings", { + method: "POST", + body: formData, + }); + const res = await POST(req); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toBe(false); + expect(body.message).toBe("Error occurred while creating new listing."); + expect(addListing).toHaveBeenCalled(); + }); }); describe("PUT /listings/[id]", () => { - test("DB connection error", async () => {}); - test("invalid id format", async () => {}); - test("invalid req body", async () => {}); - test("listing not found", async () => {}); - test("service error updating listing", async () => {}); + test("DB connection error", async () => { + const id = "123"; + const formData = new FormData(); + formData.append("itemName", "Updated Name"); + + (connectToDatabase as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "PUT", + body: formData, + }); + const res = await PUT(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toBe(false); + expect(body.message).toBe("Error connecting to database."); + }); + + test("invalid id format", async () => { + const id = "invalid-id"; + const formData = new FormData(); + formData.append("itemName", "Updated Name"); + formData.append("quantityAvailable", "10"); + formData.append("status", "ACTIVE"); + formData.append("condition", "New"); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "PUT", + body: formData, + }); + const res = await PUT(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.success).toBe(false); + expect(body.message).toBe( + "Invalid ID format. Must be a valid MongoDB ObjectId." + ); + }); + + test("invalid req body", async () => { + const id = new mongoose.Types.ObjectId().toString(); + const formData = new FormData(); + formData.append("invalidField", "Testing Invalid Value"); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "PUT", + body: formData, + }); + + const res = await PUT(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(400); + expect(body.success).toBe(false); + expect(body.message).toBe("Invalid request body."); + }); + + test("listing not found", async () => { + const id = new mongoose.Types.ObjectId().toString(); + const formData = new FormData(); + formData.append("itemName", "Updated Name"); + formData.append("quantityAvailable", "10"); + formData.append("status", "ACTIVE"); + formData.append("condition", "New"); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (updateListing as jest.Mock).mockResolvedValue(null); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "PUT", + body: formData, + }); + const res = await PUT(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(404); + expect(body.success).toBe(false); + expect(body.message).toBe("Listing not found"); + expect(updateListing).toHaveBeenCalledWith(id, { + itemName: "Updated Name", + quantityAvailable: 10, + status: "ACTIVE", + condition: "New", + }); + }); + + test("service error updating listing", async () => { + const id = new mongoose.Types.ObjectId().toString(); + const formData = new FormData(); + formData.append("itemName", "Updated Name"); + formData.append("quantityAvailable", "10"); + formData.append("status", "ACTIVE"); + formData.append("condition", "New"); + + (connectToDatabase as jest.Mock).mockResolvedValue({}); + (updateListing as jest.Mock).mockRejectedValue(new Error("DB Error")); + + const req = new Request(`http://localhost/api/listings/${id}`, { + method: "PUT", + body: formData, + }); + const res = await PUT(req, { params: { id } }); + const body = await res.json(); + + expect(res.status).toBe(500); + expect(body.success).toBe(false); + expect(body.message).toBe("Error occurred while updating listing."); + expect(updateListing).toHaveBeenCalledWith(id, { + itemName: "Updated Name", + quantityAvailable: 10, + status: "ACTIVE", + condition: "New", + }); + }); }); describe("DELETE /listings/[id]", () => { diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 33f8bd3..e3aeeff 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { connectToDatabase } from "@/lib/mongoose"; -import { number, z } from "zod"; +import { z } from "zod"; import { getFilteredListings, addListing } from "@/services/listings/listings"; import { ListingInput } from "@/models/Listing"; import { uploadImage } from "@/lib/googleCloud"; @@ -107,7 +107,9 @@ async function POST(request: Request) { // convert types (since formData changed to string) result.quantityAvailable = Number(result.quantityAvailable); - result.price = Number(result.price); + if (result.price) { + result.price = Number(result.price); + } if (result.expiryDate) { result.expiryDate = new Date(result.expiryDate as unknown as string); } From f5de9fb9ce905c48dd1668213a5cc48f31a3493f Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Wed, 22 Apr 2026 21:00:14 -0700 Subject: [PATCH 17/20] Updating authorization checks in listing API --- app/api/listings/[id]/route.ts | 34 ++++++++++++++++++++++++++++++++++ app/api/listings/route.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index 2fd9863..efea591 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -8,6 +8,7 @@ import { } from "@/services/listings/listings"; import { ListingInput } from "@/models/Listing"; import { uploadImage } from "@/lib/googleCloud"; +import { getSession } from "@/lib/rbac"; const objectIdSchema = z .string() @@ -40,6 +41,17 @@ const listingValidationSchema = z * @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 { @@ -83,6 +95,17 @@ async function GET(request: Request, { params }: { params: { id: string } }) { * @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 { @@ -198,6 +221,17 @@ 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 { diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index e3aeeff..08cf315 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { getFilteredListings, addListing } from "@/services/listings/listings"; import { ListingInput } from "@/models/Listing"; import { uploadImage } from "@/lib/googleCloud"; +import { getSession } from "@/lib/rbac"; const listingValidationSchema = z.object({ // handle defaults here for the optional fields @@ -32,6 +33,17 @@ const listingValidationSchema = z.object({ * @returns JSON response with the filtered listings as JS objects */ async function GET(request: Request) { + const { allowed, reason } = await getSession("inventory:view"); + if (!allowed) { + return NextResponse.json( + { + success: false, + message: reason, + }, + { status: 403 } + ); + } + try { await connectToDatabase(); } catch { @@ -77,6 +89,22 @@ async function GET(request: Request) { * @returns JSON response with success message and req body echoed */ async function POST(request: Request) { + let { allowed, reason } = await getSession("inventory:create"); + if (!allowed) { + return NextResponse.json( + { success: false, message: reason || "Unauthorized" }, + { status: 403 } + ); + } + + ({ allowed, reason } = await getSession("listing:create")); + if (!allowed) { + return NextResponse.json( + { success: false, message: reason || "Unauthorized" }, + { status: 403 } + ); + } + try { await connectToDatabase(); } catch { From 183f9a523cd2145c9d6d15a8d6340fe6136065e2 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Wed, 22 Apr 2026 21:02:24 -0700 Subject: [PATCH 18/20] fixup! Updating authorization checks in listing API --- app/api/listings/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index 08cf315..baa50a8 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -92,7 +92,7 @@ async function POST(request: Request) { let { allowed, reason } = await getSession("inventory:create"); if (!allowed) { return NextResponse.json( - { success: false, message: reason || "Unauthorized" }, + { success: false, message: reason }, { status: 403 } ); } @@ -100,7 +100,7 @@ async function POST(request: Request) { ({ allowed, reason } = await getSession("listing:create")); if (!allowed) { return NextResponse.json( - { success: false, message: reason || "Unauthorized" }, + { success: false, message: reason }, { status: 403 } ); } From 5501d2e9accd0c487109132414d86ef707bedbf6 Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Fri, 24 Apr 2026 08:44:35 -0700 Subject: [PATCH 19/20] Fixing failed tests by mocking RBAC in test file --- app/api/listings/__tests__/route.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/api/listings/__tests__/route.test.ts b/app/api/listings/__tests__/route.test.ts index 08c2ac5..92421ef 100644 --- a/app/api/listings/__tests__/route.test.ts +++ b/app/api/listings/__tests__/route.test.ts @@ -1,6 +1,4 @@ import mongoose from "mongoose"; -import { GET, POST } from "@/app/api/listings/route"; -import { GET as GET_BY_ID, PUT, DELETE } from "@/app/api/listings/[id]/route"; import { connectToDatabase } from "@/lib/mongoose"; /** test route handler, mock db connection and svc handlers */ @@ -21,7 +19,17 @@ jest.mock("@/lib/googleCloud", () => ({ uploadImage: jest.fn().mockResolvedValue("https://mock.com/image.jpg"), })); +jest.mock("@/lib/rbac", () => ({ + getSession: jest.fn().mockResolvedValue({ + allowed: true, + user: null, + reason: undefined, + }), +})); + /** import after mocking */ +import { GET, POST } from "@/app/api/listings/route"; +import { GET as GET_BY_ID, PUT, DELETE } from "@/app/api/listings/[id]/route"; import { getFilteredListings, getListing, From f0e130a4b113fea2ae6299b6f3d089197835524c Mon Sep 17 00:00:00 2001 From: Amormio Velasquez III Date: Fri, 24 Apr 2026 19:07:23 -0700 Subject: [PATCH 20/20] Fix validation logic for params and form data handling --- app/api/listings/[id]/route.ts | 23 +++++++------ app/api/listings/route.ts | 61 ++++++++++++++++++++++------------ services/listings/listings.ts | 15 ++++----- 3 files changed, 58 insertions(+), 41 deletions(-) diff --git a/app/api/listings/[id]/route.ts b/app/api/listings/[id]/route.ts index efea591..f9e552b 100644 --- a/app/api/listings/[id]/route.ts +++ b/app/api/listings/[id]/route.ts @@ -154,17 +154,6 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { // handle image uploads if provided const imageFiles = formData.getAll("images") as File[]; - 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; - } const parsedRequest = listingValidationSchema.safeParse(updateData); if (!parsedRequest.success) { @@ -177,6 +166,18 @@ async function PUT(request: Request, { params }: { params: { id: string } }) { ); } + 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, diff --git a/app/api/listings/route.ts b/app/api/listings/route.ts index baa50a8..556b070 100644 --- a/app/api/listings/route.ts +++ b/app/api/listings/route.ts @@ -26,6 +26,15 @@ const listingValidationSchema = z.object({ .default([]), }); +const paramsValidationSchema = z + .object({ + labId: z.string().optional(), + itemId: z.string().optional(), + page: z.number().int().default(1), + limit: z.number().int().default(10), + }) + .strict(); + /** * Get filtered listings stored in db * @param request the request @@ -54,18 +63,28 @@ async function GET(request: Request) { } const { searchParams } = new URL(request.url); - const labId = searchParams.get("labId") || undefined; - const itemId = searchParams.get("itemId") || undefined; - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); + + const parsedParams = paramsValidationSchema.safeParse({ + labId: searchParams.get("labId") ?? undefined, + itemId: searchParams.get("itemId") ?? undefined, + page: searchParams.get("page") ?? undefined, + limit: searchParams.get("limit") ?? undefined, + }); + + if (!parsedParams.success) { + return NextResponse.json( + { + success: false, + message: "Invalid request params.", + }, + { status: 400 } + ); + } try { - const { listings, pagination } = await getFilteredListings({ - labId, - itemId, - page, - limit, - }); + const { listings, pagination } = await getFilteredListings( + parsedParams.data + ); return NextResponse.json( { @@ -89,7 +108,7 @@ async function GET(request: Request) { * @returns JSON response with success message and req body echoed */ async function POST(request: Request) { - let { allowed, reason } = await getSession("inventory:create"); + let { allowed, reason } = await getSession("listing:create"); if (!allowed) { return NextResponse.json( { success: false, message: reason }, @@ -143,18 +162,8 @@ async function POST(request: Request) { } const imageFiles = formData.getAll("images") as File[]; - 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); - } - result.imageUrls = imageUrls; - } const parsedBody = listingValidationSchema.safeParse(result); - if (!parsedBody.success) { return NextResponse.json( { @@ -165,6 +174,16 @@ async function POST(request: Request) { ); } + 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); + } + result.imageUrls = imageUrls; + } + try { const listingData = { ...parsedBody.data, diff --git a/services/listings/listings.ts b/services/listings/listings.ts index 20f6e7d..081fa67 100644 --- a/services/listings/listings.ts +++ b/services/listings/listings.ts @@ -34,17 +34,14 @@ async function getFilteredListings({ if (labId) query.labId = labId; if (itemId) query.itemId = itemId; - const MAX_LIMIT = 20; // inquire about this in the future - const validPage = isNaN(page) || page < 1 ? 1 : page; - const validLimit = - isNaN(limit) || limit < 1 ? 10 : Math.min(limit, MAX_LIMIT); - const skip = (validPage - 1) * validLimit; + const newLimit = Math.min(limit, 20); + const skip = (page - 1) * newLimit; const [listings, total] = await Promise.all([ ListingModel.find(query) .sort({ createdAt: -1 }) // Sort from newest to oldest .skip(skip) - .limit(validLimit) + .limit(newLimit) .exec(), // toListing handles doc to JS object, so lean unncessary ListingModel.countDocuments(query), ]); @@ -52,10 +49,10 @@ async function getFilteredListings({ return { listings: listings.map((listing) => toListing(listing)), pagination: { - page: validPage, - limit: validLimit, + page: page, + limit: newLimit, total, - totalPages: Math.ceil(total / validLimit), + totalPages: Math.ceil(total / newLimit), }, }; }