Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: find catalog product regardless of visibility #6089

Merged
merged 16 commits into from
Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 66 additions & 66 deletions package-lock.json

Large diffs are not rendered by default.

99 changes: 7 additions & 92 deletions src/core-services/cart/xforms/xformCartItems.js
Original file line number Diff line number Diff line change
@@ -1,103 +1,18 @@
import ReactionError from "@reactioncommerce/reaction-error";

/**
* @name xformCatalogProductMedia
* @method
* @memberof GraphQL/Transforms
* @summary Transforms DB media object to final GraphQL result. Calls functions plugins have registered for type
* "xformCatalogProductMedia". First to return an object is returned here
* @param {Object} mediaItem Media item object. See ImageInfo SimpleSchema
* @param {Object} context Request context
* @returns {Object} Transformed media item
*/
async function xformCatalogProductMedia(mediaItem, context) {
const xformCatalogProductMediaFuncs = context.getFunctionsOfType("xformCatalogProductMedia");
for (const func of xformCatalogProductMediaFuncs) {
const xformedMediaItem = await func(mediaItem, context); // eslint-disable-line no-await-in-loop
if (xformedMediaItem) {
return xformedMediaItem;
}
}

return mediaItem;
}

/**
* @param {Object} context - an object containing the per-request state
* @param {Object[]} catalogItems Array of CatalogItem docs from the db
* @param {Object[]} products Array of Product docs from the db
* @param {Object} cartItem CartItem
* @returns {Object} Same object with GraphQL-only props added
*/
async function xformCartItem(context, catalogItems, products, cartItem) {
const { productId, variantId } = cartItem;

const catalogItem = catalogItems.find((cItem) => cItem.product.productId === productId);
if (!catalogItem) {
throw new ReactionError("not-found", `CatalogProduct with product ID ${productId} not found`);
}

const catalogProduct = catalogItem.product;

const { variant } = context.queries.findVariantInCatalogProduct(catalogProduct, variantId);
if (!variant) {
throw new ReactionError("invalid-param", `Product with ID ${productId} has no variant with ID ${variantId}`);
}

// Find one image from the catalog to use for the item.
// Prefer the first variant image. Fallback to the first product image.
let media;
if (variant.media && variant.media.length) {
[media] = variant.media;
} else if (catalogProduct.media && catalogProduct.media.length) {
media = catalogProduct.media.find((mediaItem) => mediaItem.variantId === variantId);
if (!media) [media] = catalogProduct.media;
}

// Allow plugins to transform the media object
if (media) {
media = await xformCatalogProductMedia(media, context);
}

return {
...cartItem,
imageURLs: media && media.URLs,
productConfiguration: {
productId: cartItem.productId,
productVariantId: cartItem.variantId
}
};
}

/**
* @param {Object} context - an object containing the per-request state
* @param {Object[]} items Array of CartItem
* @returns {Object[]} Same array with GraphQL-only props added
*/
export default async function xformCartItems(context, items) {
const { collections, getFunctionsOfType } = context;
const { Catalog, Products } = collections;

const productIds = items.map((item) => item.productId);

const catalogItems = await Catalog.find({
"product.productId": {
$in: productIds
},
"product.isVisible": true,
"product.isDeleted": { $ne: true },
"isDeleted": { $ne: true }
}).toArray();

const products = await Products.find({
ancestors: {
$in: productIds
const xformedItems = items.map((item) => ({
...item,
productConfiguration: {
productId: item.productId,
productVariantId: item.variantId
}
}).toArray();

const xformedItems = await Promise.all(items.map((item) => xformCartItem(context, catalogItems, products, item)));
}));

for (const mutateItems of getFunctionsOfType("xformCartItems")) {
for (const mutateItems of context.getFunctionsOfType("xformCartItems")) {
await mutateItems(context, xformedItems); // eslint-disable-line no-await-in-loop
}

Expand Down
16 changes: 8 additions & 8 deletions src/core-services/catalog/mutations/hashProduct.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import hash from "object-hash";
import { customPublishedProductFields, customPublishedProductVariantFields } from "../registration.js";
import getCatalogProductMedia from "../utils/getCatalogProductMedia.js";
import getTopLevelProduct from "../utils/getTopLevelProduct.js";

const productFieldsThatNeedPublishing = [
Expand Down Expand Up @@ -55,12 +54,12 @@ const variantFieldsThatNeedPublishing = [
* @method createProductHash
* @summary Create a hash of a product to compare for updates
* @memberof Catalog
* @param {String} product - The Product document to hash. Expected to be a top-level product, not a variant
* @param {Object} collections - Raw mongo collections
* @param {Object} context App context
* @param {String} product The Product document to hash. Expected to be a top-level product, not a variant
* @returns {String} product hash
*/
export async function createProductHash(product, collections) {
const variants = await collections.Products.find({ ancestors: product._id, type: "variant" }).toArray();
export async function createProductHash(context, product) {
const variants = await context.collections.Products.find({ ancestors: product._id, type: "variant" }).toArray();

const productForHashing = {};
productFieldsThatNeedPublishing.forEach((field) => {
Expand All @@ -70,9 +69,6 @@ export async function createProductHash(product, collections) {
productForHashing[field] = product[field];
});

// Track changes to all related media, too
productForHashing.media = await getCatalogProductMedia(product._id, collections);

// Track changes to all variants, too
productForHashing.variants = variants.map((variant) => {
const variantForHashing = {};
Expand All @@ -85,6 +81,10 @@ export async function createProductHash(product, collections) {
return variantForHashing;
});

for (const func of context.getFunctionsOfType("mutateProductHashObject")) {
await func(context, { productForHashing, product }); // eslint-disable-line no-await-in-loop
}

return hash(productForHashing);
}

Expand Down
39 changes: 0 additions & 39 deletions src/core-services/catalog/mutations/hashProduct.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import mockContext from "@reactioncommerce/api-utils/tests/mockContext.js";
import {
rewire as rewire$getCatalogProductMedia,
restore as restore$getCatalogProductMedia
} from "../utils/getCatalogProductMedia.js";
import { rewire as rewire$getTopLevelProduct, restore as restore$getTopLevelProduct } from "../utils/getTopLevelProduct.js";
import hashProduct, { rewire$createProductHash, restore as restore$hashProduct } from "./hashProduct.js";

Expand All @@ -13,7 +9,6 @@ const opaqueShopId = "cmVhY3Rpb24vc2hvcDoxMjM="; // reaction/shop:123
const internalCatalogItemId = "999";
const internalProductId = "999";
const internalTagIds = ["923", "924"];
const internalVariantIds = ["875", "874"];

const productSlug = "fake-product";

Expand Down Expand Up @@ -53,20 +48,6 @@ const mockProduct = {
weight: 7.77
},
pinterestMsg: "pinterestMessage",
media: [
{
metadata: {
priority: 1,
productId: internalProductId,
variantId: null
},
thumbnail: "http://localhost/thumbnail",
small: "http://localhost/small",
medium: "http://localhost/medium",
large: "http://localhost/large",
image: "http://localhost/original"
}
],
productId: internalProductId,
productType: "productType",
shop: {
Expand All @@ -88,36 +69,16 @@ const mockProduct = {
}
};

const mockGetCatalogProductMedia = jest
.fn()
.mockName("getCatalogProductMedia")
.mockReturnValue(Promise.resolve([
{
priority: 1,
productId: internalProductId,
variantId: internalVariantIds[1],
URLs: {
large: "large/path/to/image.jpg",
medium: "medium/path/to/image.jpg",
original: "image/path/to/image.jpg",
small: "small/path/to/image.jpg",
thumbnail: "thumbnail/path/to/image.jpg"
}
}
]));

const mockCreateProductHash = jest.fn().mockName("createProductHash").mockReturnValue("fake_hash");
const mockGetTopLevelProduct = jest.fn().mockName("getTopLevelProduct").mockReturnValue(mockProduct);

beforeAll(() => {
rewire$createProductHash(mockCreateProductHash);
rewire$getCatalogProductMedia(mockGetCatalogProductMedia);
rewire$getTopLevelProduct(mockGetTopLevelProduct);
});

afterAll(() => {
restore$hashProduct();
restore$getCatalogProductMedia();
restore$getTopLevelProduct();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ export default async function findCatalogProductsAndVariants(context, variants)
const { collections: { Catalog } } = context;
const productIds = variants.map((variant) => variant.productId);

const catalogProductItems = await Catalog.find({
"product.productId": { $in: productIds },
"product.isVisible": true,
"product.isDeleted": { $ne: true },
"isDeleted": { $ne: true }
}).toArray();
const catalogProductItems = await Catalog.find({ "product.productId": { $in: productIds } }).toArray();

const catalogProductsAndVariants = catalogProductItems.map((catalogProductItem) => {
const { product } = catalogProductItem;
Expand Down
28 changes: 8 additions & 20 deletions src/core-services/catalog/utils/createCatalogProduct.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import Logger from "@reactioncommerce/logger";
import getCatalogProductMedia from "./getCatalogProductMedia.js";

/**
* @method
* @summary Converts a variant Product document into the catalog schema for variants
* @param {Object} variant The variant from Products collection
* @param {Object} variantMedia Media for this specific variant
* @private
* @returns {Object} The transformed variant
*/
export function xformVariant(variant, variantMedia) {
const primaryImage = variantMedia[0] || null;

export function xformVariant(variant) {
return {
_id: variant._id,
attributeLabel: variant.attributeLabel,
Expand All @@ -20,12 +16,12 @@ export function xformVariant(variant, variantMedia) {
height: variant.height,
index: variant.index || 0,
length: variant.length,
media: variantMedia,
media: variant.media || [],
metafields: variant.metafields,
minOrderQuantity: variant.minOrderQuantity,
optionTitle: variant.optionTitle,
originCountry: variant.originCountry,
primaryImage,
primaryImage: variant.primaryImage || null,
shopId: variant.shopId,
sku: variant.sku,
title: variant.title,
Expand All @@ -45,11 +41,7 @@ export function xformVariant(variant, variantMedia) {
* @param {Object[]} data.variants The Product documents for all variants of this product
* @returns {Object} The CatalogProduct document
*/
export async function xformProduct({ context, product, variants }) {
const { collections } = context;
const catalogProductMedia = await getCatalogProductMedia(product._id, collections);
const primaryImage = catalogProductMedia[0] || null;

export async function xformProduct({ product, variants }) {
const topVariants = [];
const options = new Map();

Expand All @@ -69,15 +61,11 @@ export async function xformProduct({ context, product, variants }) {
const catalogProductVariants = topVariants
// We want to explicitly map everything so that new properties added to variant are not published to a catalog unless we want them
.map((variant) => {
const variantMedia = catalogProductMedia.filter((media) => media.variantId === variant._id);
const newVariant = xformVariant(variant, variantMedia);
const newVariant = xformVariant(variant);

const variantOptions = options.get(variant._id);
if (variantOptions) {
newVariant.options = variantOptions.map((option) => {
const optionMedia = catalogProductMedia.filter((media) => media.variantId === option._id);
return xformVariant(option, optionMedia);
});
newVariant.options = variantOptions.map((option) => xformVariant(option));
}

return newVariant;
Expand All @@ -93,13 +81,13 @@ export async function xformProduct({ context, product, variants }) {
isDeleted: !!product.isDeleted,
isVisible: !!product.isVisible,
length: product.length,
media: catalogProductMedia,
media: product.media || [],
metafields: product.metafields,
metaDescription: product.metaDescription,
originCountry: product.originCountry,
pageTitle: product.pageTitle,
parcel: product.parcel,
primaryImage,
primaryImage: product.primaryImage || null,
// The _id prop could change whereas this should always point back to the source product in Products collection
productId: product._id,
productType: product.productType,
Expand Down
Loading