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

Feature: add bulk mutations to manage products and tags #5404

Merged
merged 15 commits into from
Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import executeBulkOperation from "../utils/executeBulkOperation";

/**
*
* @method addTagsToProducts
* @summary Adds an array of tag ids to an array of products looked up by product id.
* @param {Object} context - an object containing the per-request state
* @param {Object} input - Input arguments for the bulk operation
* @param {String[]} input.productIds - an array of Product IDs
* @param {String[]} input.tagIds - an array of Tag IDs
* @return {Object} Object with information of results of bulk the operation
*/
export default async function addTagsToProducts(context, input) {
const { productIds, tagIds } = input;
const { collections: { Products } } = context;
const totalProducts = productIds.length;

// // Generate update statements
const operations = productIds.map((productId) => ({
updateOne: {
filter: {
_id: productId
},
update: {
$addToSet: {
hashtags: { $each: tagIds }
}
}
}
}));

const results = await executeBulkOperation(Products, operations, totalProducts);

return results;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import mockContext from "/imports/test-utils/helpers/mockContext";
import addTagsToProducts from "./addTagsToProducts";

const mockInput = {
input: {
productIds: ["BCTMZ6HTxFSppJESk", "XWTMZ6HTxFSppJESo"],
tagIds: ["cseCBSSrJ3t8HQSNP", "YCeCBSSrJ3t8HQSxx"]
}
};

const expectedResults = {
foundCount: 2,
notFoundCount: 0,
updatedCount: 2,
writeErrors: []
};

test("Testing addTagsToProducts, returns info on the results of the bulk write", async () => {
const { input } = mockInput;
const results = await addTagsToProducts(mockContext, input);

expect(mockContext.collections.Products.bulkWrite).toHaveBeenCalled();
expect(results).toEqual(expectedResults);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import addTagsToProducts from "./addTagsToProducts";
import removeTagsFromProducts from "./removeTagsFromProducts";

export default {
addTagsToProducts,
removeTagsFromProducts
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import executeBulkOperation from "../utils/executeBulkOperation";

/**
*
* @method removeTagsFromProducts
* @summary Removes an array of tag ids to an array of products looked up by product id.
* @param {Object} context - an object containing the per-request state
* @param {Object} input - Input arguments for the bulk operation
* @param {String[]} input.productIds - an array of Product IDs
* @param {String[]} input.tagIds - an array of Tag IDs
* @return {Object} Object with information of results of bulk the operation
*/
export default async function removeTagsFromProducts(context, input) {
const { productIds, tagIds } = input;
const { collections: { Products } } = context;
const totalProducts = productIds.length;

// Generate update statements
const operations = productIds.map((productId) => ({
updateOne: {
filter: {
_id: productId
},
update: {
$pull: {
hashtags: { $in: tagIds }
}
},
multi: true
}
}));

const results = await executeBulkOperation(Products, operations, totalProducts);

return results;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import mockContext from "/imports/test-utils/helpers/mockContext";
import removeTagsFromProducts from "./removeTagsFromProducts";

const mockInput = {
input: {
productIds: ["BCTMZ6HTxFSppJESk", "XWTMZ6HTxFSppJESo"],
tagIds: ["cseCBSSrJ3t8HQSNP", "YCeCBSSrJ3t8HQSxx"]
}
};

const expectedResults = {
foundCount: 2,
notFoundCount: 0,
updatedCount: 2,
writeErrors: []
};

test("Testing removeTagsFromProducts, returns info on the results of the bulk write", async () => {
const { input } = mockInput;
const results = await removeTagsFromProducts(mockContext, input);

expect(mockContext.collections.Products.bulkWrite).toHaveBeenCalled();
expect(results).toEqual(expectedResults);
});
4 changes: 3 additions & 1 deletion imports/plugins/core/product/server/no-meteor/register.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import mutations from "./mutations";
import resolvers from "./resolvers";
import schemas from "./schemas";

Expand Down Expand Up @@ -28,6 +29,7 @@ export default async function register(app) {
graphQL: {
resolvers,
schemas
}
},
mutations
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
*
* @method addTagsToProducts
* @summary Takes an array of productsIds and tagsIds
* and performs a bulk operation to add an array of tag ids to an
* array of products
* @param {Object} _ - unused
* @param {Object} args - The input arguments
* @param {Object} args.input - mutation input object
* @param {String} args.input.clientMutationId - The mutation id
* @param {String[]} args.input.productIds - an array of Product IDs
* @param {String[]} args.input.tagIds - an array of Tag IDs
* @param {Object} context - an object containing the per-request state
* @return {Promise<Object>} Returns an object with information about the results
* of the bulk operation
*/
export default async function addTagsToProducts(_, { input }, context) {
const { clientMutationId } = input;

const results = await context.mutations.addTagsToProducts(context, input);

return {
clientMutationId,
...results
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import addTagsToProducts from "./addTagsToProducts";
import removeTagsFromProducts from "./removeTagsFromProducts";

export default {
addTagsToProducts,
removeTagsFromProducts
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
*
* @method removeTagsFromProducts
* @summary Takes an array of productsIds and tagsIds
* and removes provided tags from the array of products.
* @param {Object} _ - unused
* @param {Object} args - The input arguments
* @param {Object} args.input - mutation input object
* @param {String} args.input.clientMutationId - The mutation id
* @param {String[]} args.input.productIds - an array of Product IDs
* @param {String[]} args.input.tagIds - an array of Tag IDs
* @param {Object} context - an object containing the per-request state
* @return {Promise<Object>} Returns an object with information about the results
* of the bulk operation
*/
export default async function removeTagsFromProducts(_, { input }, context) {
const { clientMutationId } = input;

const results = await context.mutations.removeTagsFromProducts(context, input);

return {
clientMutationId,
...results
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ProductConfiguration from "./ProductConfiguration";
import Mutation from "./Mutation";

export default {
ProductConfiguration
ProductConfiguration,
Mutation
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,56 @@ type ProductConfiguration {
"The ProductVariant ID"
productVariantId: ID!
}

"A bulk write error type"
type WriteError {
"The documentId(_id) on which the error occurred"
documentId: Int

"Error message for a documentId"
errorMsg: String
}

"Input for adding tags to products in bulk"
input ProductTagsOperationInput {
"An optional string identifying the mutation call, which will be returned in the response payload"
clientMutationId: String

"An array of product productIds to which an array of tags will be added"
productIds: [ID]

"An array of tag ids to add to an array of products"
willopez marked this conversation as resolved.
Show resolved Hide resolved
tagIds: [ID]
}

"Response payload managing tags on products"
type ProductTagsOperationPayload {
"The same string you sent with the mutation params, for matching mutation calls with their responses"
clientMutationId: String

"The number of products found"
foundCount: Int

"The number of products for which a match was not found"
notFoundCount: Int

"The number of products successfully updated"
updatedCount: Int

"An array of write errors if any were generated"
writeErrors: [WriteError]
willopez marked this conversation as resolved.
Show resolved Hide resolved
}

extend type Mutation {
"Bulk operation for adding an array of tags to an array of products"
addTagsToProducts(
"input which must includes an array of product ids and an array of tag ids"
input: ProductTagsOperationInput!
): ProductTagsOperationPayload!

"Bulk operation for removing an array of tags from an array of products"
removeTagsFromProducts(
"input which must includes an array of product ids and an array of tag ids"
input: ProductTagsOperationInput!
): ProductTagsOperationPayload!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Logger from "@reactioncommerce/logger";

const logCtx = { name: "core/product", file: " executeBulkOperation" };

/**
*
* @method executeBulkOperation
* @summary Executes an array of operations in bulk
* @param {Object[]} collection Mongo collection
* @param {Object[]} operations bulk operations to perform
* @param {Int} totalProducts total number of products to operate on
* @return {Object} Object with information of results of bulk the operations
*/
export default async function executeBulkOperation(collection, operations, totalProducts) {
let response;
try {
Logger.trace({ ...logCtx, operations }, "Running bulk operation");
response = await collection.bulkWrite(operations, { ordered: false });
} catch (error) {
Logger.error({ ...logCtx, error }, "One or more of the bulk update failed");
response = error; // error object has details about failed & successful operations
}

const { nMatched, nModified, result: { writeErrors } } = response;
const notFoundCount = totalProducts - nMatched;

const cleanedErrors = writeErrors.map((error) => ({
documentId: error.op._id,
errorMsg: error.errmsg
}));

return {
foundCount: nMatched,
notFoundCount,
updatedCount: nModified,
writeErrors: cleanedErrors
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,13 @@ class ProductGridItems extends Component {

render() {
const { isSelected, product } = this.props;
const isChecked = isSelected() === "active" || false;
const productItem = (
<TableRow className={`product-table-row-item ${isSelected() ? "active" : ""}`}>
<TableCell padding="checkbox">
<Checkbox
onClick={this.handleSelect}
checked={isSelected()}
checked={isChecked}
/>
</TableCell>
<TableCell align="center">
Expand Down
7 changes: 7 additions & 0 deletions imports/test-utils/helpers/mockContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export function mockCollection(collectionName) {
update() {
throw new Error("update mongo method is deprecated, use updateOne or updateMany");
},
bulkWrite: jest.fn().mockName(`${collectionName}.bulkWrite`).mockReturnValue(Promise.resolve({
nMatched: 2,
nModified: 2,
result: {
writeErrors: []
}
})),
deleteOne: jest.fn().mockName(`${collectionName}.deleteOne`).mockReturnValue(Promise.resolve({
deletedCount: 1
})),
Expand Down