Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions imports/node-app/core-services/core/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@
"addVariantOption": "Add option",
"addVariantFail": "Unable to create variant. {{err}}",
"addVariant": "Variant added",
"archiveProductsFail": "Unable to archive product(s). {{err}}",
"archiveProductsSuccess": "Product(s) archived successfully.",
"archiveProductVariantsFail": "Unable to archive variant(s). {{err}}",
"archiveProductVariantsSuccess": "Variant(s) archived successfully.",
"cloneProductSuccess": "Product(s) cloned successfully.",
"cloneProductFail": "Unable to clone product. {{err}}",
"cloneVariantFail": "Unable to clone variant for {{title}}.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`throws if permission check fails 1`] = `"Access Denied"`;

exports[`throws if the productIds isn't supplied 1`] = `"Product ids is required"`;

exports[`throws if the shopId isn't supplied 1`] = `"Shop ID is required"`;
123 changes: 123 additions & 0 deletions imports/node-app/core-services/product/mutations/archiveProducts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import SimpleSchema from "simpl-schema";
import ReactionError from "@reactioncommerce/reaction-error";

const inputSchema = new SimpleSchema({
"productIds": Array,
"productIds.$": {
type: String
},
"shopId": String
});

/**
*
* @method archiveProducts
* @summary archives a product
* @description the method archives products, but will also archive
* child variants and options
* @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 decoded product IDs to archive
* @param {String} input.shopId - shop these products belong to
* @return {Array} array with archived products
*/
export default async function archiveProducts(context, input) {
inputSchema.validate(input);
const { appEvents, collections, userHasPermission, userId } = context;
const { MediaRecords, Products } = collections;
const { productIds, shopId } = input;

if (!userHasPermission(["createProduct", "product/admin", "product/archive"], shopId)) {
throw new ReactionError("access-denied", "Access Denied");
}

// Check to make sure all products are on the same shop
const count = await Products.find({ _id: { $in: productIds }, shopId }).count();
if (count !== productIds.length) throw new ReactionError("not-found", "One or more products do not exist");

// Find all products that aren't deleted, and all their variants variants
const productsWithVariants = await Products.find({
// Don't "archive" products that are already marked deleted.
isDeleted: {
$ne: true
},
$or: [
{
_id: {
$in: productIds
}
},
{
ancestors: {
$in: productIds
}
}
]
}).toArray();

// Get ID's of all products to archive
const productIdsToArchive = productsWithVariants.map((product) => product._id);


const archivedProducts = await Promise.all(productIdsToArchive.map(async (productId) => {
const { value: archivedProduct } = await Products.findOneAndUpdate(
{
_id: productId
},
{
$set: {
isDeleted: true
}
}, {
returnOriginal: false
}
);

if (archivedProduct.type === "variant") {
appEvents.emit("afterVariantSoftDelete", {
variant: {
...archivedProduct
},
deletedBy: userId
});
} else {
appEvents.emit("afterProductSoftDelete", {
product: {
...archivedProduct
},
deletedBy: userId
});
}
return archivedProduct;
}));

if (archivedProducts && archivedProducts.length) {
// Flag associated MediaRecords as deleted.
await MediaRecords.updateMany(
{
$or: [
{
"metadata.productId": {
$in: productIdsToArchive
}
},
{
"metadata.variantId": {
$in: productIdsToArchive
}
}
]
},
{
$set: {
"metadata.isDeleted": true,
"metadata.workflow": "archived"
}
}
);
}

// Return only originally supplied product(s),
// not variants and options also archived
return archivedProducts.filter((archivedProduct) => productIds.includes(archivedProduct._id));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import mockContext from "@reactioncommerce/api-utils/tests/mockContext.js";
import archiveProducts from "./archiveProducts.js";

mockContext.mutations.archiveProducts = jest.fn().mockName("mutations.archiveProducts");

test("throws if permission check fails", async () => {
mockContext.userHasPermission.mockReturnValueOnce(false);

await expect(archiveProducts(mockContext, {
productIds: ["PRODUCT_ID_1", "PRODUCT_ID_2"],
shopId: "SHOP_ID"
})).rejects.toThrowErrorMatchingSnapshot();

expect(mockContext.userHasPermission).toHaveBeenCalledWith(["createProduct", "product/admin", "product/archive"], "SHOP_ID");
});

test("throws if the productIds isn't supplied", async () => {
mockContext.userHasPermission.mockReturnValueOnce(true);

await expect(archiveProducts(mockContext, {
productIds: undefined,
shopId: "SHOP_ID"
})).rejects.toThrowErrorMatchingSnapshot();
});

test("throws if the shopId isn't supplied", async () => {
mockContext.userHasPermission.mockReturnValueOnce(true);

await expect(archiveProducts(mockContext, {
productIds: ["PRODUCT_ID_1", "PRODUCT_ID_2"],
shopId: undefined
})).rejects.toThrowErrorMatchingSnapshot();
});
2 changes: 2 additions & 0 deletions imports/node-app/core-services/product/mutations/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import addTagsToProducts from "./addTagsToProducts.js";
import archiveProducts from "./archiveProducts.js";
import cloneProducts from "./cloneProducts.js";
import cloneProductVariants from "./cloneProductVariants.js";
import createProduct from "./createProduct.js";
Expand All @@ -7,6 +8,7 @@ import removeTagsFromProducts from "./removeTagsFromProducts.js";

export default {
addTagsToProducts,
archiveProducts,
cloneProducts,
cloneProductVariants,
createProduct,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { decodeProductOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/product";
import { decodeShopOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/shop";

/**
*
* @method archiveProductVariants
* @summary Takes an array of variant IDs and archives variants
* @param {Object} _ - unused
* @param {Object} args - The input arguments
* @param {Object} args.input - mutation input object
* @param {String} args.input.shopId - shop these variants belong to
* @param {String} args.input.variantIds - an array of variant IDs to archive
* @param {Object} context - an object containing the per-request state
* @return {Array} array with archived variants
*/
export default async function archiveProductVariants(_, { input }, context) {
const {
clientMutationId,
shopId,
variantIds
} = input;

const decodedVariantIds = variantIds.map((variantId) => decodeProductOpaqueId(variantId));

// This `archiveProductVariants` resolver calls the `archiveProducts` mutation
// as we don't have the need to separate this into `archiveProductVariants` at this time.
// In the future, we can create a `archiveProductVariants` mutation if needed.
const archivedVariants = await context.mutations.archiveProducts(context, {
productIds: decodedVariantIds,
shopId: decodeShopOpaqueId(shopId)
});

return {
clientMutationId,
variants: archivedVariants
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { decodeProductOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/product";
import { decodeShopOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/shop";

/**
*
* @method archiveProducts
* @summary Takes an array of product IDs and archives products
* @param {Object} _ - unused
* @param {Object} args - The input arguments
* @param {Object} args.input - mutation input object
* @param {String} args.input.productIds - an array of decoded product IDs to archive
* @param {String} args.input.shopId - shop these products belong to
* @param {Object} context - an object containing the per-request state
* @return {Array} array with archived products
*/
export default async function archiveProducts(_, { input }, context) {
const {
clientMutationId,
productIds,
shopId
} = input;

const decodedProductIds = productIds.map((productId) => decodeProductOpaqueId(productId));

const archivedProducts = await context.mutations.archiveProducts(context, {
productIds: decodedProductIds,
shopId: decodeShopOpaqueId(shopId)
});

return {
clientMutationId,
products: archivedProducts
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import addTagsToProducts from "./addTagsToProducts.js";
import archiveProducts from "./archiveProducts.js";
import archiveProductVariants from "./archiveProductVariants.js";
import cloneProducts from "./cloneProducts.js";
import cloneProductVariants from "./cloneProductVariants.js";
import createProduct from "./createProduct.js";
Expand All @@ -7,6 +9,8 @@ import removeTagsFromProducts from "./removeTagsFromProducts.js";

export default {
addTagsToProducts,
archiveProducts,
archiveProductVariants,
cloneProducts,
cloneProductVariants,
createProduct,
Expand Down
48 changes: 48 additions & 0 deletions imports/node-app/core-services/product/schemas/product.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ type ProductVariant {
width: Float
}

"Response payload of `archiveProducts` mutation"
type ArchiveProductsPayload {
"The same string you sent with the mutation params, for matching mutation calls with their responses"
clientMutationId: String

"Array of newly archived products"
products: [Product]!
}

"Response payload of `archiveProductVariants` mutation"
type ArchiveProductVariantsPayload {
"The same string you sent with the mutation params, for matching mutation calls with their responses"
clientMutationId: String

"Array of newly archived variants"
variants: [ProductVariant]!
}

"Response payload of `createProduct` mutation"
type CreateProductPayload {
"The same string you sent with the mutation params, for matching mutation calls with their responses"
Expand Down Expand Up @@ -198,6 +216,24 @@ type CloneProductVariantsPayload {
variants: [ProductVariant]!
}

"Input for the `archiveProducts` mutation"
input ArchiveProductsInput {
"Array of IDs of products to archive"
productIds: [ID]!

"ID of shop that owns all products you are archiving"
shopId: ID!
}

"Input for the `archiveProducts` mutation"
input ArchiveProductVariantsInput {
"ID of shop that owns all variants you are archiving"
shopId: ID!

"Array of IDs of variants to archive"
variantIds: [ID]!
}

"Input for the `createProduct` mutation"
input CreateProductInput {
"ID of shop product will belong to"
Expand Down Expand Up @@ -232,6 +268,18 @@ input CloneProductVariantsInput {
}

extend type Mutation {
"Archive products"
archiveProducts(
"Mutation input"
input: ArchiveProductsInput!
): ArchiveProductsPayload!

"Archive product variants"
archiveProductVariants(
"Mutation input"
input: ArchiveProductVariantsInput!
): ArchiveProductVariantsPayload!

"Create a new product"
createProduct(
"Mutation input"
Expand Down
6 changes: 6 additions & 0 deletions imports/node-app/plugins/job-queue/api.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Logger from "@reactioncommerce/logger";
import config from "./config.js";
import { Job, Jobs } from "./jobs.js";

const { REACTION_WORKERS_ENABLED } = config;

/**
* @summary Add a worker for a background job type
* @param {Object} options Worker options
Expand All @@ -13,6 +16,9 @@ import { Job, Jobs } from "./jobs.js";
* @return {Promise<Job>} A promise that resolves with the worker instance
*/
export function addWorker(options) {
// To disable background workers when running integration tests
if (!REACTION_WORKERS_ENABLED) return Promise.resolve();

const {
pollInterval = 5 * 60 * 1000, // default 5 minutes
type,
Expand Down
12 changes: 12 additions & 0 deletions imports/node-app/plugins/job-queue/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import envalid, { bool, str } from "envalid";

export default envalid.cleanEnv(process.env, {
// This is necessary to override the envalid default
// validation for NODE_ENV, which uses
// str({ choices: ['development', 'test', 'production'] })
//
// We currently need to set NODE_ENV to "jesttest" when
// integration tests run.
NODE_ENV: str(),
REACTION_WORKERS_ENABLED: bool({ default: true })
});
Loading