Introducción
RIDGE & REED vende ropa para actividades al aire libre. Un cliente quiere un artículo verde de talla mediana que tenga existencias y cueste como máximo 5000 centavos. Un producto puede tener una variante verde y una variante mediana con existencias sin tener disponible una variante verde mediana. Incluso después de elegir el producto correcto, mostrar todo su array de variantes o su opción más barata sin existencias seguiría induciendo a error al cliente.
Guardarás un selector de productos reutilizable y después crearás un pipeline de agregación de MongoDB que devuelva todos los SKU que cumplan los requisitos y calcule el precio más bajo de esas mismas variantes. Necesitas conocimientos básicos para moverte por la terminal, sobre conexiones de MongoDB, objetos y arrays, y sobre find/update. Las plantillas de funciones JavaScript, las expresiones de agregación y las etapas se explican antes de usarlas. Usa Terminal durante todo el laboratorio; MongoDB, las existencias sintéticas y los clientes de solo lectura están preparados.
Modifica únicamente selector.js y finder.js. Conserva todos los productos originales, los ID de variante, los atributos, las existencias y los precios, y las herramientas proporcionadas. La disponibilidad es una cantidad de existencias preparada, no una garantía de reserva ni de compra. Cada variante real tiene un SKU, un color y una talla de texto, y cantidades enteras no negativas de existencias y precio en centavos. Las variantes de un producto pueden ser un array, null o un campo ausente; otros tipos de entrada malformada quedan fuera de este ejercicio.
Mantener todas las condiciones sobre una misma variante
En este paso, compararás la solicitud de un cliente con un producto real y guardarás un selector reutilizable que rechace una coincidencia engañosa formada con distintas variantes. Empieza en Terminal. cd cambia de directorio; vuelve a usar esta ruta absoluta si entras en el directorio equivocado:
cd /home/labex/project/variant-desk
./catalog es un lector proporcionado en este directorio; ./ significa el directorio actual. Dale un ID de producto para leer el documento realmente almacenado:
./catalog TRAIL-1
Trail Jacket tiene estas tres variantes distintas:
| SKU | Color | Talla | Existencias | Precio en centavos |
|---|---|---|---|---|
| T-GM | green | M | 0 | 1800 |
| T-BM | blue | M | 5 | 2200 |
| T-GL | green | L | 4 | 2100 |
La variante verde mediana no tiene existencias. La azul mediana y la verde grande están disponibles, pero ninguna satisface la solicitud completa. Un SKU identifica una variante; un ID de producto identifica su producto padre. Conserva esas relaciones.
./db conecta el intérprete de MongoDB con el catálogo de solo lectura de este curso. --eval ejecuta el JavaScript entre comillas y sale. Las comillas simples del intérprete del sistema protegen los espacios y los operadores de MongoDB que empiezan por dólar frente a la expansión del intérprete; las comillas dobles del interior pertenecen a cadenas JavaScript. Las rutas con puntos como "variants.color" acceden a campos anidados. $gt: 0 significa mayor que cero; $lte: 5000 significa menor o igual que 5000 e incluye el límite del presupuesto. Los valores numéricos son números, no cadenas de moneda entre comillas.
Esta consulta ordinaria con rutas de puntos es deliberadamente demasiado amplia. El segundo argumento de find proyecta solo el ID de producto: 1 incluye un campo y _id: 0 omite el ID interno de MongoDB. sort con 1 ordena de forma ascendente; toArray() reúne los documentos devueltos en un array legible:
./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()'
Deberías ver CAMP-3, RIDGE-2 y TRAIL-1. TRAIL-1 es la coincidencia falsa: distintos elementos del array aportan distintas condiciones. La consulta se ejecutó correctamente, pero su respuesta de negocio es errónea.
$elemMatch aplica todos los criterios a un mismo elemento del array. Puede que ya conozcas este operador por los fundamentos de arrays de MongoDB; aquí es la primera pieza de un resultado completo destinado al cliente:
./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()'
Ahora solo quedan CAMP-3 y RIDGE-2. Un producto coincidente tiene al menos una variante que cumple los requisitos. Esto todavía no significa que todas sus variantes los cumplan.
Guarda un selector que reciba la solicitud como argumento en lugar de fijar green/M/5000 dentro de la consulta. Una función JavaScript es código reutilizable: function buildSelector(request) le da nombre y recibe un objeto llamado request; request.color lee la propiedad de color de ese objeto. return proporciona el objeto de consulta al cliente preparado. Las llaves contienen objetos o el cuerpo de la función, los dos puntos conectan las claves de objeto con sus valores y las comas separan propiedades. La interfaz requerida de la función es buildSelector(request), que devuelve un objeto de consulta. No es un comando del intérprete del sistema ni una exportación de módulo.
En el intérprete del sistema, cat > reemplaza el archivo de destino. <<'JS' inicia un here-document literal que conserva los signos de dólar y las comillas. Pega el JS final solo en una línea; introduce el bloque completo en el indicador del intérprete del sistema:
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 lee esa función guardada y ejecuta su selector devuelto sin cambios mediante find de MongoDB. Sus tres argumentos son el color, la talla y el precio máximo en centavos. El cliente convierte el presupuesto en un número; las cadenas como green y M siguen siendo etiquetas exactas que distinguen mayúsculas de minúsculas:
./select green M 5000
Los documentos que realmente coinciden son CAMP-3 y RIDGE-2. Siguen apareciendo sus arrays originales completos, incluidas las variantes que no coinciden. Eso es normal en find: la selección elige documentos, no los elementos que se mostrarán. Conserva el selector como un resultado útil y aborda el problema de visualización en el siguiente paso.
Si falta un archivo, usa ls para listar este directorio y comprueba el nombre de archivo y el directorio de trabajo absoluto. Ante un error de sintaxis JavaScript, reemplaza el bloque guardado completo por su versión corregida y vuelve a ejecutar el cliente. Si el intérprete sigue esperando, introduce la línea final JS que falta. No cambies las existencias fuente, borres variantes ni reinicies la base de datos para que una consulta parezca correcta.
Devolver todos los SKU que cumplen los requisitos
En este paso, convertirás la selección de productos en un resultado completo de variantes, sin devolver solo la primera coincidencia ni incluir todo el array. Empieza en la misma Terminal, en /home/labex/project/variant-desk.
Consulta las dos coincidencias reales:
./catalog RIDGE-2
./catalog CAMP-3
Para green/M con un máximo de 5000 centavos, Ridge Jacket tiene dos SKU que cumplen los requisitos: R-GM1 a 4500 y R-GM2 a 5000. R-GM3 es demasiado caro; las otras tallas o colores y la variante sin existencias no cumplen los requisitos. Camp Vest tiene C-GM1 y C-GM2, ambos a 4000. Los precios iguales no convierten SKU diferentes en duplicados.
Una proyección posicional, "variants.$": 1, devuelve solo el primer elemento coincidente del array. Observa por qué no puede entregar el conjunto completo de esta solicitud:
./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()'
Cada producto tiene solo una variante en esta salida. Falta el segundo SKU legítimo aunque esté disponible. Devolver todo el array original fallaría en el sentido contrario, al incluir variantes no deseadas.
Un pipeline de agregación es un array de etapas procesadas en orden. Cada etapa recibe los documentos de la etapa anterior. $project elige los campos de salida y puede calcular un campo nuevo. $filter calcula un array que contiene todos los elementos cuya condición es verdadera; aquí no se aplica ningún límite.
Dentro de $filter, input es el array fuente y as: "v" da nombre a la variante actual. "$variants" hace referencia a un campo del documento; "$$v.color" hace referencia al color del elemento actual del array. Estos signos de dólar son expresiones de MongoDB dentro de cadenas JavaScript. request.color es un valor JavaScript de la solicitud proporcionado por el cliente.
Las comparaciones de agregación usan un array de operandos: { $eq: [left, right] } comprueba la igualdad, $gt comprueba si es mayor que y $lte si es menor o igual. $and exige que todas las comparaciones sean verdaderas para ese mismo v. Esto difiere de la sintaxis de objeto de consulta de $elemMatch, aunque las condiciones de negocio son idénticas.
$ifNull: ["$variants", []] sustituye por un array vacío un campo fuente null o ausente. [] es un array JavaScript vacío. Un valor que no sea un array, como una cadena, queda fuera del contrato de entrada proporcionado; este pipeline no pretende limpiar catálogos malformados arbitrarios.
Después del filtrado, $match elimina del resultado los productos sin variantes que cumplan los requisitos. "variants.0" hace referencia al índice cero, el primer elemento del array; $exists: true exige que exista. Es un predicado de consulta en una etapa match, no una expresión dentro de la condición de filtrado. El $sort final facilita la lectura de la salida real.
Guarda la plantilla completa en finder.js. Su interfaz requerida es buildPipeline(request), que devuelve un array de etapas. El cliente ejecutará este pipeline devuelto sin cambios; no filtra su respuesta después:
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
Deberías ver exactamente dos productos. CAMP-3 contiene C-GM1 y C-GM2; RIDGE-2 contiene R-GM1 y R-GM2. Cada variante devuelta conserva su SKU, color, talla, existencias y precio en centavos exactos. El producto conserva sus product_id y title exactos. Todavía no hay un campo de precio mínimo; ese es el siguiente paso.
El pipeline devuelve ahora los cuatro SKU que cumplen los requisitos, incluidas las dos variantes de Camp Vest del mismo precio y el SKU de Ridge exactamente en el límite del presupuesto. TRAIL-1, los arrays vacíos y los arrays null o ausentes no producen productos en el resultado. No se borró ni editó ningún documento fuente para crear este resultado.
Si el cliente indica que buildPipeline debe devolver un array de pipeline, comprueba que el archivo guardado incluya la función entera y return [ ... ];. Si informa de un error de sintaxis, reemplaza el bloque completo y vuelve a intentarlo. Mantén el JavaScript de consulta en el archivo guardado; no añadas comandos del intérprete del sistema, un comando interactivo use ni exportaciones de módulo.
Calcular el precio del conjunto coincidente y reutilizar el buscador
En este paso, añadirás el precio más bajo que cumpla los requisitos y usarás la misma consulta guardada para diferentes solicitudes de cliente, incluido un resultado vacío real. Empieza en Terminal, en /home/labex/project/variant-desk.
La variante original más barata de Ridge Jacket cuesta 900 centavos, pero no tiene existencias. Otra variante barata tiene el color equivocado. Anunciar cualquiera de ellas como precio inicial para green/M induciría a error al cliente aunque el propio producto se hubiera seleccionado correctamente.
$set añade o reemplaza un campo de salida en una etapa de agregación; no actualiza el producto almacenado. Aquí añade min_price_cents. La expresión $min con un único operando array devuelve su valor numérico más pequeño. "$variants.price_cents" accede a los precios dentro del array de variantes del documento actual. Coloca esta etapa después del filtrado: debe leer el array coincidente producido por $project, no el array original. Usar una etapa posterior también permite acceder sin ambigüedad a ese campo modificado.
Reemplaza finder.js con el pipeline completo siguiente. La única etapa nueva calcula el mínimo después de excluir los arrays vacíos:
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
El resultado debe conservar los cuatro SKU que cumplen los requisitos y añadir el mínimo 4000 de CAMP-3 y el mínimo 4500 de RIDGE-2. min_price_cents se deriva del conjunto de variantes devuelto de cada producto. No elimines duplicados por precios iguales, uses un mínimo de todo el producto ni escribas a mano esos dos números.
Ahora reduce el presupuesto usando la misma función. Cambian los valores de la solicitud; la consulta guardada no cambia:
./finder green M 4499
Solo queda CAMP-3, todavía con ambos SKU de 4000 centavos y un mínimo de 4000. Ridge Jacket no tiene ninguna variante green/M que cumpla los requisitos por debajo de este nuevo límite.
Cambia el color y el límite:
./finder blue M 2500
RIDGE-2 ahora contiene solo R-BM a 1000; TRAIL-1 contiene solo T-BM a 2200. Un producto correctamente excluido para una solicitud puede incluirse para otra. La disponibilidad pertenece a una relación específica entre variante y solicitud, no a un indicador permanente del producto.
Por último, prueba un presupuesto sin coincidencias:
./finder green M 500
El resultado real es []. El cliente no sustituye el resultado por uno de demostración ni muestra artículos sin disponibilidad para llenar la pantalla. Una consulta ausente o un error de base de datos se comunica como error, no se disfraza de este array vacío correcto.
El resultado final de tu pipeline contiene exactamente product_id, title, variants y min_price_cents. Devuelve cada producto coincidente una vez y cada SKU que cumpla los requisitos una vez con los cinco campos originales de variante. El orden de productos y variantes es flexible; las identidades y los valores son el contrato de negocio. La consulta también debe gestionar otro catálogo con el mismo esquema e ID, precios y existencias diferentes. Los clientes preparados leen resultados reales de MongoDB y no corrigen la selección, los arrays ni los precios.
Conserva ambos archivos JavaScript guardados y todos los productos y herramientas originales. Has creado una consulta de catálogo reutilizable, no un sistema de reservas: los cambios concurrentes de existencias o una compra posterior todavía podrían afectar a la disponibilidad. El desafío siguiente tiene su propio inventario de alquiler independiente y usa los mismos mecanismos de consulta con nombres de atributo y valores de solicitud diferentes.
Resumen
Rechazaste las coincidencias que mezclan variantes, conservaste todos los SKU que cumplían los requisitos en lugar de solo el primero y calculaste los precios mostrados a partir de arrays filtrados. Tus funciones guardadas gestionan solicitudes diferentes, precios iguales y resultados vacíos, conservando el catálogo original.



