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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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 };