Find Every Available Product Variant

MongoDBBeginner
Practice Now

Introduction

RIDGE & REED sells outdoor clothing. A customer wants a green, medium-sized item that is in stock and costs no more than 5000 cents. A product can have a green variant and a medium variant in stock without having a green medium variant available. Even after choosing the correct product, displaying its entire variant array or its cheapest unavailable option would still mislead the customer.

You will save a reusable product selector, then build a MongoDB aggregation pipeline that returns all eligible SKUs and calculates the lowest price from those same variants. You need basic terminal navigation, MongoDB connections, objects/arrays and basic find/update knowledge. JavaScript function templates, aggregation expressions and stages are explained before use. Use Terminal throughout; MongoDB, synthetic stock and read-only clients are prepared.

Change only selector.js and finder.js. Preserve all original products, variant IDs, attributes, stock and prices, and the supplied tools. Availability is a prepared stock count, not a reservation or checkout guarantee. Each actual variant has a text SKU/color/size and nonnegative integer stock/price cents. A product's variants can be an array, null or missing; other malformed input types are outside this exercise.

Keep Every Condition on One Variant

In this step, you will compare a customer's request with a real product and save a reusable selector that rejects a misleading cross-variant match. Begin in Terminal. cd changes directory; use this absolute path again if you enter a wrong directory:

cd /home/labex/project/variant-desk

./catalog is a supplied reader in this directory; ./ means the current directory. Give it a product ID to read the actual stored document:

./catalog TRAIL-1

Trail Jacket has these three distinct variants:

SKU Color Size Stock Price cents
T-GM green M 0 1800
T-BM blue M 5 2200
T-GL green L 4 2100

The green medium variant is out of stock. The blue medium and green large variants are available, but neither satisfies the complete request. A SKU identifies one variant; a product ID identifies its parent product. Keep those relationships.

./db connects the MongoDB shell to this course's read-only catalog. --eval runs the quoted JavaScript and exits. Single shell quotes protect spaces and MongoDB's dollar-prefixed operators from shell expansion; double quotes inside belong to JavaScript strings. Dot paths such as "variants.color" reach nested fields. $gt: 0 means greater than zero; $lte: 5000 means less than or equal to 5000, including the budget boundary. Numeric values are numbers, not quoted currency strings.

This ordinary dot-path query is deliberately too broad. The second find argument projects only the product ID: 1 includes a field and _id: 0 omits MongoDB's internal ID. sort with 1 orders ascending; toArray() collects the returned documents into one readable array:

./db --eval 'db.products.find({"variants.color":"green","variants.size":"M","variants.stock":{$gt:0},"variants.price_cents":{$lte:5000}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'

Expect CAMP-3, RIDGE-2 and TRAIL-1. TRAIL-1 is the false match: different array elements supply different conditions. The query ran successfully, but its business answer is wrong.

$elemMatch puts all criteria on one array element. This operator may already be familiar from MongoDB array fundamentals; here it is the first building block of a complete customer-facing result:

./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'

Now only CAMP-3 and RIDGE-2 remain. A matching product has at least one eligible variant. This does not yet mean every variant in that product is eligible.

Save a selector that takes the request as an argument instead of fixing green/M/5000 inside the query. A JavaScript function is reusable code: function buildSelector(request) names it and receives an object called request; request.color reads that object's color property. return supplies the query object to the prepared client. Braces contain objects or the function body, colons connect object keys to values, and commas separate properties. The function's required interface is buildSelector(request) returning one query object. It is not a shell command or module export.

In the shell, cat > replaces the destination file. <<'JS' begins a literal here-document, preserving dollar signs and quotes. Paste the final JS alone on a line; enter the whole block at the shell prompt:

cat > selector.js <<'JS'
function buildSelector(request) {
  return {
    variants: { $elemMatch: {
      color: request.color,
      size: request.size,
      stock: { $gt: 0 },
      price_cents: { $lte: request.max_cents }
    } }
  };
}
JS

./select reads that saved function and executes its returned selector unchanged with MongoDB find. Its three arguments are color, size and maximum price cents. The client converts the budget to a number; strings such as green and M remain exact, case-sensitive labels:

./select green M 5000

The actual matching documents are CAMP-3 and RIDGE-2. Their entire original arrays still appear, including variants that do not match. That is normal for find: selection chooses documents, not which elements to display. Keep the selector as a useful result, then address the display problem in the next step.

If a file is missing, use ls to list this directory and check the filename and absolute working directory. For a JavaScript syntax error, replace the complete saved block with its corrected form and rerun the client. If the shell is still waiting, enter the missing final JS line. Do not change source stock, delete variants or reset the database to make a query appear correct.

Return Every Eligible SKU

In this step, you will turn product selection into a complete variant result, without returning only the first match or leaking the entire array. Begin in the same Terminal at /home/labex/project/variant-desk.

Read the two genuine matches:

./catalog RIDGE-2
./catalog CAMP-3

For green/M with a maximum of 5000 cents, Ridge Jacket has two eligible SKUs: R-GM1 at 4500 and R-GM2 at 5000. R-GM3 is too expensive; other sizes/colors and the zero-stock variant do not qualify. Camp Vest has C-GM1 and C-GM2, both at 4000. Equal prices do not make different SKUs duplicates.

A positional projection, "variants.$": 1, returns only the first matching array element. See why it cannot deliver this request's full set:

./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1,"variants.$":1}).sort({product_id:1}).toArray()'

Each product has only one variant in this output. The second legitimate SKU is missing even though it is available. Returning the whole original array would fail in the other direction by including unwanted variants.

An aggregation pipeline is an array of stages processed in order. Each stage receives the previous stage's documents. $project chooses output fields and can compute a new field. $filter computes an array containing every element whose condition is true; no limit is applied here.

Inside $filter, input is the source array and as: "v" names the current variant. "$variants" refers to a document field; "$$v.color" refers to the color of the current array element. These dollar signs are MongoDB expressions within JavaScript strings. request.color is a JavaScript request value supplied by the client.

Aggregation comparisons use an operand array: { $eq: [left, right] } checks equality, $gt checks greater than, and $lte checks less than or equal. $and requires every comparison to be true for that same v. This differs from the query-object spelling in $elemMatch, although the business conditions are identical.

$ifNull: ["$variants", []] substitutes an empty array for a null or missing source field. [] is an empty JavaScript array. A non-array value such as a string is outside the supplied input contract; this pipeline does not claim to clean arbitrary malformed catalogs.

After filtering, $match removes products with no eligible variants. "variants.0" refers to index zero, the first array element; $exists: true requires it to exist. This is a query predicate in a match stage, not an expression inside the filter condition. The final $sort makes the actual output convenient to read.

Save the complete template in finder.js. Its required interface is buildPipeline(request) returning an array of stages. The client will execute this returned pipeline unchanged; it does not filter its answer afterward:

cat > finder.js <<'JS'
function buildPipeline(request) {
  return [
    { $project: {
        _id: 0, product_id: 1, title: 1,
        variants: { $filter: {
          input: { $ifNull: ["$variants", []] },
          as: "v",
          cond: { $and: [
            { $eq: ["$$v.color", request.color] },
            { $eq: ["$$v.size", request.size] },
            { $gt: ["$$v.stock", 0] },
            { $lte: ["$$v.price_cents", request.max_cents] }
          ] }
        } }
    } },
    { $match: { "variants.0": { $exists: true } } },
    { $sort: { product_id: 1 } }
  ];
}
JS
./finder green M 5000

Expect exactly two products. CAMP-3 contains C-GM1 and C-GM2; RIDGE-2 contains R-GM1 and R-GM2. Each returned variant retains its exact SKU, color, size, stock and price cents. The product retains its exact product_id and title. There is no minimum-price field yet; that is the next step.

The pipeline now returns all four eligible SKUs, including the two equal-price Camp Vest variants and the Ridge SKU exactly at the budget. TRAIL-1, empty arrays and null/missing arrays produce no result products. No source document was deleted or edited to create this result.

If the client reports that buildPipeline must return a pipeline array, check that the saved file includes the whole function and return [ ... ];. If it reports a syntax error, replace the complete block and retry. Keep query JavaScript in the saved file; do not add shell commands, an interactive use command or module exports.

Price the Matching Set and Reuse the Finder

In this step, you will add the lowest eligible price and use the same saved query for different customer requests, including an honest empty result. Begin in Terminal in /home/labex/project/variant-desk.

Ridge Jacket's cheapest original variant costs 900 cents but has zero stock. Another cheap variant is the wrong color. Advertising either as the green/M starting price would mislead the customer even if the product itself was correctly selected.

$set adds or replaces an output field in an aggregation stage; it does not update the stored product. Here it adds min_price_cents. The $min expression with one array operand returns its smallest numeric value. "$variants.price_cents" reaches the prices inside the current document's variant array. Place this stage after filtering: it must read the matching array produced by $project, not the original array. A later stage also makes that changed field available unambiguously.

Replace finder.js with the complete pipeline below. The only new stage calculates the minimum after the empty arrays have been excluded:

cat > finder.js <<'JS'
function buildPipeline(request) {
  return [
    { $project: {
        _id: 0, product_id: 1, title: 1,
        variants: { $filter: {
          input: { $ifNull: ["$variants", []] },
          as: "v",
          cond: { $and: [
            { $eq: ["$$v.color", request.color] },
            { $eq: ["$$v.size", request.size] },
            { $gt: ["$$v.stock", 0] },
            { $lte: ["$$v.price_cents", request.max_cents] }
          ] }
        } }
    } },
    { $match: { "variants.0": { $exists: true } } },
    { $set: { min_price_cents: { $min: "$variants.price_cents" } } },
    { $sort: { product_id: 1 } }
  ];
}
JS
./finder green M 5000

The result must keep all four eligible SKUs and add CAMP-3's minimum 4000 and RIDGE-2's minimum 4500. min_price_cents is derived from each returned variant set. Do not deduplicate equal prices, use a product-wide minimum or handwrite those two numbers.

Now lower the budget using the same function. The request's values change; the saved query does not:

./finder green M 4499

Only CAMP-3 remains, still with both 4000-cent SKUs and minimum 4000. Ridge Jacket has no eligible green/M variant below this new cap.

Change the color and cap:

./finder blue M 2500

RIDGE-2 now contains only R-BM at 1000; TRAIL-1 contains only T-BM at 2200. A product correctly excluded for one request can be included for another. Availability belongs to a specific variant/request relationship, not a permanent product flag.

Finally, try a budget with no matches:

./finder green M 500

The actual result is []. The client does not substitute a demo result or show unavailable items to fill the screen. A missing query or database error is reported as an error, not disguised as this successful empty array.

Your final pipeline result contains exactly product_id, title, variants and min_price_cents. Return every matching product once and every eligible SKU once with all five original variant fields. Product/variant order is flexible; identities and values are the business contract. The query must also handle another catalog with the same schema and different IDs, prices and stock. The prepared clients read real MongoDB results and do not correct selection, arrays or prices.

Keep both saved JavaScript files and all original products/tools. You have built a reusable catalog query, not a reservation system: concurrent stock changes or a later checkout could still affect availability. The following Challenge has its own independent rental inventory and uses the same query mechanisms with different attribute names and request values.

Summary

You rejected cross-variant matches, kept every eligible SKU instead of only the first, and calculated display prices from filtered arrays. Your saved functions handle changed requests, equal prices and empty results while preserving the original catalog.