Adds ProductVariantService
This commit is contained in:
33
packages/medusa/src/models/__mocks__/product-variant.js
Normal file
33
packages/medusa/src/models/__mocks__/product-variant.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
export const ProductVariantModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query._id === IdMap.getId("validId")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("validId"),
|
||||
title: "test",
|
||||
})
|
||||
}
|
||||
if (query._id === IdMap.getId("testVariant")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("testVariant"),
|
||||
title: "test",
|
||||
})
|
||||
}
|
||||
if (query._id === IdMap.getId("deleteId")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("deleteId"),
|
||||
title: "test",
|
||||
})
|
||||
}
|
||||
if (query._id === IdMap.getId("failId")) {
|
||||
return Promise.reject(new Error("test error"))
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import IdMap from "../../../helpers/id-map"
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
export const ProductModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
@@ -15,6 +15,8 @@ class ProductVariantModel extends BaseModel {
|
||||
prices: { type: [MoneyAmountSchema], default: [], required: true },
|
||||
options: { type: [OptionValueSchema], default: [] },
|
||||
image: { type: String, default: "" },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
published: { type: Boolean, default: false },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
product_id: { type: mongoose.Types.ObjectId, required: true },
|
||||
title: { type: String, required: true },
|
||||
values: { type: [String], default: [] },
|
||||
})
|
||||
|
||||
@@ -60,6 +60,21 @@ const variant4 = {
|
||||
],
|
||||
}
|
||||
|
||||
const variant5 = {
|
||||
_id: "5",
|
||||
title: "Variant with valid id",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "50",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const invalidVariant = {
|
||||
_id: "invalid_option",
|
||||
title: "variant3",
|
||||
@@ -104,6 +119,9 @@ export const ProductVariantServiceMock = {
|
||||
if (variantId === "4") {
|
||||
return Promise.resolve(variant4)
|
||||
}
|
||||
if (variantId === IdMap.getId("validId")) {
|
||||
return Promise.resolve(variant5)
|
||||
}
|
||||
if (variantId === "invalid_option") {
|
||||
return Promise.resolve(invalidVariant)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
export const ProductServiceMock = {
|
||||
createDraft: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
list: jest.fn().mockImplementation(data => {
|
||||
if (data.variants === IdMap.getId("testVariant")) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
_id: "1234",
|
||||
title: "test",
|
||||
options: [
|
||||
{
|
||||
_id: IdMap.getId("testOptionId"),
|
||||
title: "testOption",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import IdMap from "../../../helpers/id-map"
|
||||
|
||||
const variant1 = {
|
||||
_id: "1",
|
||||
title: "variant1",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "160",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const variant2 = {
|
||||
_id: "2",
|
||||
title: "variant2",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "black",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "160",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const variant3 = {
|
||||
_id: "3",
|
||||
title: "variant3",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "150",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const variant4 = {
|
||||
_id: "4",
|
||||
title: "variant4",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "50",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const invalidVariant = {
|
||||
_id: "invalid_option",
|
||||
title: "variant3",
|
||||
options: [
|
||||
{
|
||||
option_id: "invalid_id",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "150",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const emptyVariant = {
|
||||
_id: "empty_option",
|
||||
title: "variant3",
|
||||
options: [],
|
||||
}
|
||||
|
||||
export const variants = {
|
||||
one: variant1,
|
||||
two: variant2,
|
||||
three: variant3,
|
||||
four: variant4,
|
||||
invalid_variant: invalidVariant,
|
||||
empty_variant: emptyVariant,
|
||||
}
|
||||
|
||||
export const ProductVariantServiceMock = {
|
||||
retrieve: jest.fn().mockImplementation(variantId => {
|
||||
if (variantId === "1") {
|
||||
return Promise.resolve(variant1)
|
||||
}
|
||||
if (variantId === "2") {
|
||||
return Promise.resolve(variant2)
|
||||
}
|
||||
if (variantId === "3") {
|
||||
return Promise.resolve(variant3)
|
||||
}
|
||||
if (variantId === "4") {
|
||||
return Promise.resolve(variant4)
|
||||
}
|
||||
if (variantId === "invalid_option") {
|
||||
return Promise.resolve(invalidVariant)
|
||||
}
|
||||
if (variantId === "empty_option") {
|
||||
return Promise.resolve(emptyVariant)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
delete: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
addOptionValue: jest.fn().mockImplementation((variantId, optionId, value) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
}
|
||||
416
packages/medusa/src/services/__tests__/product-variant.js
Normal file
416
packages/medusa/src/services/__tests__/product-variant.js
Normal file
@@ -0,0 +1,416 @@
|
||||
import mongoose from "mongoose"
|
||||
import ProductVariantService from "../product-variant"
|
||||
import { ProductVariantModelMock } from "../../models/__mocks__/product-variant"
|
||||
import IdMap from "../../helpers/id-map"
|
||||
import { ProductServiceMock } from "../__mocks__/product"
|
||||
|
||||
describe("ProductVariantService", () => {
|
||||
describe("retrieve", () => {
|
||||
describe("successfully get product variant", () => {
|
||||
let res
|
||||
beforeAll(async () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
res = await productVariantService.retrieve(IdMap.getId("validId"))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls model layer findOne", () => {
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("validId"),
|
||||
})
|
||||
})
|
||||
|
||||
it("returns correct variant", () => {
|
||||
expect(res.title).toEqual("test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("query fail", () => {
|
||||
let res
|
||||
beforeAll(async () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
await productVariantService
|
||||
.retrieve(IdMap.getId("failId"))
|
||||
.catch(err => {
|
||||
res = err
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls model layer findOne", () => {
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("failId"),
|
||||
})
|
||||
})
|
||||
|
||||
it("model query throws error", () => {
|
||||
expect(res.name).toEqual("database_error")
|
||||
expect(res.message).toEqual("test error")
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("createDraft", () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks()
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
productVariantService.createDraft({
|
||||
title: "Test Prod",
|
||||
image: "test-image",
|
||||
options: [],
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("calls model layer create", () => {
|
||||
expect(ProductVariantModelMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.create).toHaveBeenCalledWith({
|
||||
title: "Test Prod",
|
||||
image: "test-image",
|
||||
options: [],
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
published: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("publishVariant", () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks()
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
productVariantService.publish(IdMap.getId("variantId"))
|
||||
})
|
||||
|
||||
it("calls model layer create", () => {
|
||||
expect(ProductVariantModelMock.create).toHaveBeenCalledTimes(0)
|
||||
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("variantId") },
|
||||
{ $set: { published: true } }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls updateOne with correct params", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
|
||||
await productVariantService.update(`${id}`, { title: "new title" })
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: `${id}` },
|
||||
{ $set: { title: "new title" } },
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error on invalid variant id type", async () => {
|
||||
try {
|
||||
await productVariantService.update(19314235, { title: "new title" })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The variantId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws error when trying to update metadata", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
try {
|
||||
await productVariantService.update(`${id}`, {
|
||||
metadata: { key: "value" },
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("Use setMetadata to update metadata fields")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("decorate", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
const fakeVariant = {
|
||||
_id: "1234",
|
||||
title: "test",
|
||||
image: "test-image",
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
metadata: { testKey: "testValue" },
|
||||
published: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns decorated product", async () => {
|
||||
const decorated = await productVariantService.decorate(fakeVariant, [])
|
||||
expect(decorated).toEqual({
|
||||
_id: "1234",
|
||||
metadata: { testKey: "testValue" },
|
||||
})
|
||||
})
|
||||
|
||||
it("returns decorated product with handle", async () => {
|
||||
const decorated = await productVariantService.decorate(fakeVariant, [
|
||||
"prices",
|
||||
])
|
||||
expect(decorated).toEqual({
|
||||
_id: "1234",
|
||||
metadata: { testKey: "testValue" },
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("setMetadata", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls updateOne with correct params", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
await productVariantService.setMetadata(
|
||||
`${id}`,
|
||||
"metadata",
|
||||
"testMetadata"
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: `${id}` },
|
||||
{ $set: { "metadata.metadata": "testMetadata" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error on invalid key type", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
|
||||
try {
|
||||
await productVariantService.setMetadata(`${id}`, 1234, "nono")
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws error on invalid variantId type", async () => {
|
||||
try {
|
||||
await productVariantService.setMetadata("fakeVariantId", 1234, "nono")
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The variantId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("addOptionValue", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
productService: ProductServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("it successfully adds option value", async () => {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId"),
|
||||
"testValue"
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testVariant") },
|
||||
{
|
||||
$push: {
|
||||
options: {
|
||||
option_id: IdMap.getId("testOptionId"),
|
||||
value: "testValue",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("it successfully casts numeric option value to string", async () => {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId"),
|
||||
1234
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testVariant") },
|
||||
{
|
||||
$push: {
|
||||
options: {
|
||||
option_id: IdMap.getId("testOptionId"),
|
||||
value: "1234",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error if product with variant does not exist", async () => {
|
||||
try {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("failId"),
|
||||
IdMap.getId("testOptionId"),
|
||||
"testValue"
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Products with variant: ${IdMap.getId("failId")} was not found`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throw error if product does not have option id", async () => {
|
||||
try {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("failOptionId"),
|
||||
"testValue"
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Associated product does not have option: ${IdMap.getId(
|
||||
"failOptionId"
|
||||
)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throw error if option value is not string", async () => {
|
||||
try {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId"),
|
||||
{}
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Option value is not of type string or number`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteOptionValue", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
productService: ProductServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully deletes option value from variant", async () => {
|
||||
await productVariantService.deleteOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testing")
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testVariant") },
|
||||
{ $pull: { options: { option_id: IdMap.getId("testing") } } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error if product still has the option id of the option value we are trying to delete", async () => {
|
||||
try {
|
||||
await productVariantService.deleteOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId")
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Associated product has option with id: ${IdMap.getId(
|
||||
"testOptionId"
|
||||
)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("deletes all variants and product successfully", async () => {
|
||||
await productVariantService.delete(IdMap.getId("deleteId"))
|
||||
|
||||
expect(ProductVariantModelMock.deleteOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.deleteOne).toBeCalledWith({
|
||||
_id: IdMap.getId("deleteId"),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import mongoose from "mongoose"
|
||||
import ProductService from "../product"
|
||||
import { ProductModelMock } from "./mocks/product-model"
|
||||
import { ProductModelMock } from "../../models/__mocks__/product"
|
||||
import {
|
||||
ProductVariantServiceMock,
|
||||
variants,
|
||||
} from "./mocks/product-variant-service"
|
||||
} from "../__mocks__/product-variant"
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
describe("ProductService", () => {
|
||||
@@ -107,6 +107,7 @@ describe("ProductService", () => {
|
||||
jest.clearAllMocks()
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
productService.publish(IdMap.getId("productId"))
|
||||
@@ -189,7 +190,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("add metadata to product model", () => {
|
||||
describe("setMetadata", () => {
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
})
|
||||
@@ -232,7 +233,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("update product", () => {
|
||||
describe("update", () => {
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
})
|
||||
@@ -285,7 +286,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete product", () => {
|
||||
describe("delete", () => {
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
@@ -307,16 +308,6 @@ describe("ProductService", () => {
|
||||
_id: IdMap.getId("deleteId"),
|
||||
})
|
||||
})
|
||||
|
||||
it("throw error on invalid product id type", async () => {
|
||||
try {
|
||||
await productService.update(19314235, { title: "new title" })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The productId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("addVariant", () => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "../interfaces"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate products.
|
||||
* Provides layer to manipulate product variants.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class ProductVariantService extends BaseService {
|
||||
/** @param { productModel: (ProductModel) } */
|
||||
constructor({ productVariantModel, eventBusService }) {
|
||||
/** @param { productVariantModel: (ProductVariantModel) } */
|
||||
constructor({ productVariantModel, eventBusService, productService }) {
|
||||
super()
|
||||
|
||||
/** @private @const {ProductVariantModel} */
|
||||
@@ -14,36 +16,250 @@ class ProductVariantService extends BaseService {
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @const {ProductService} */
|
||||
this.productService_ = productService
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unpublished product.
|
||||
* @param {object} product - the product to create
|
||||
* Used to validate product ids. Throws an error if the cast fails
|
||||
* @param {string} rawId - the raw product id to validate.
|
||||
* @return {string} the validated id
|
||||
*/
|
||||
validateId_(rawId) {
|
||||
const schema = Validator.objectId()
|
||||
const { value, error } = schema.validate(rawId)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"The variantId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a product variant by id.
|
||||
* @param {string} variantId - the id of the product to get.
|
||||
* @return {Promise<Product>} the product document.
|
||||
*/
|
||||
retrieve(variantId) {
|
||||
const validatedId = this.validateId_(variantId)
|
||||
return this.productVariantModel_
|
||||
.findOne({ _id: validatedId })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unpublished product variant.
|
||||
* @param {object} variant - the variant to create
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
createDraft(productVariant) {
|
||||
return this.productVariantModel_.create({
|
||||
...productVariant,
|
||||
published: false,
|
||||
})
|
||||
return this.productVariantModel_
|
||||
.create({
|
||||
...productVariant,
|
||||
published: false,
|
||||
})
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an publishes product.
|
||||
* @param {string} productId - ID of the product to publish.
|
||||
* Creates an publishes variant.
|
||||
* @param {string} variantId - ID of the variant to publish.
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
publish(variantId) {
|
||||
return this.productVariantModel_
|
||||
.updateOne({ _id: variantId }, { $set: { published: true } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a variant. Metadata updates and price updates should
|
||||
* use dedicated methods, e.g. `setMetadata`, etc. The function
|
||||
* will throw errors if metadata updates and price updates are attempted.
|
||||
* @param {string} variantId - the id of the variant. Must be a string that
|
||||
* can be casted to an ObjectId
|
||||
* @param {object} update - an object with the update values.
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
update(variantId, update) {
|
||||
const validatedId = this.validateId_(variantId)
|
||||
|
||||
if (update.metadata) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
|
||||
return this.productVariantModel_
|
||||
.updateOne(
|
||||
{ _id: validatedId },
|
||||
{ $set: update },
|
||||
{ runValidators: true }
|
||||
)
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds option value to a varaint.
|
||||
* Fails when product with variant does not exists or
|
||||
* if that product does not have an option with the given
|
||||
* option id. Fails if given variant is not found.
|
||||
* Option value must be of type string or number.
|
||||
* @param {string} variantId - the variant to decorate.
|
||||
* @param {string} optionId - the option from product.
|
||||
* @param {string | number} optionValue - option value to add.
|
||||
* @return {Promise} the result of the update operation.
|
||||
*/
|
||||
async addOptionValue(variantId, optionId, optionValue) {
|
||||
const products = await this.productService_.list({ variants: variantId })
|
||||
if (!products.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Products with variant: ${variantId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
const product = products[0]
|
||||
if (!product.options.find(o => o._id === optionId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Associated product does not have option: ${optionId}`
|
||||
)
|
||||
}
|
||||
|
||||
const variant = await this.retrieve(variantId)
|
||||
if (!variant) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variant with ${variantId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof optionValue !== "string" && typeof optionValue !== "number") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Option value is not of type string or number`
|
||||
)
|
||||
}
|
||||
|
||||
return this.productVariantModel_.updateOne(
|
||||
{ _id: variantId },
|
||||
{ $set: { published: true } }
|
||||
{ $push: { options: { option_id: optionId, value: `${optionValue}` } } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Deletes option value from given variant.
|
||||
* Fails when product with variant does not exists or
|
||||
* if that product has an option with the given
|
||||
* option id.
|
||||
* This method should only be used from the product service.
|
||||
* @param {string} variantId - the variant to decorate.
|
||||
* @param {string} optionId - the option from product.
|
||||
* @return {Promise} the result of the update operation.
|
||||
*/
|
||||
addOptionValue(variantId, optionId, optionValue) {
|
||||
async deleteOptionValue(variantId, optionId) {
|
||||
const products = await this.productService_.list({ variants: variantId })
|
||||
if (!products.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Products with variant: ${variantId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
const product = products[0]
|
||||
if (product.options.find(o => o._id === optionId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Associated product has option with id: ${optionId}`
|
||||
)
|
||||
}
|
||||
|
||||
return this.productVariantModel_.updateOne(
|
||||
{ _id: variantId },
|
||||
{ $pull: { options: { option_id: optionId } } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
list(selector) {
|
||||
return this.productVariantModel_.find(selector)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a variant from given variant id.
|
||||
* @param {string} variantId - the id of the variant to delete. Must be
|
||||
* castable as an ObjectId
|
||||
* @return {Promise} the result of the delete operation.
|
||||
*/
|
||||
async delete(variantId) {
|
||||
const variant = await this.retrieve(variantId)
|
||||
// Delete is idempotent, but we return a promise to allow then-chaining
|
||||
if (!variant) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return this.productVariantModel_
|
||||
.deleteOne({ _id: variantId })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates a variant with variant variants.
|
||||
* @param {ProductVariant} variant - the variant to decorate.
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @return {ProductVariant} return the decorated variant.
|
||||
*/
|
||||
async decorate(variant, fields, expandFields = []) {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(variant, fields.concat(requiredFields))
|
||||
return decorated
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata for a variant.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} variantId - the variant to decorate.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
setMetadata(variantId, key, value) {
|
||||
const validatedId = this.validateId_(variantId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
|
||||
const keyPath = `metadata.${key}`
|
||||
return this.productVariantModel_
|
||||
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@ class ProductService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
list(query) {
|
||||
const selector = {}
|
||||
list(selector) {
|
||||
return this.productModel_.find(selector)
|
||||
}
|
||||
|
||||
@@ -477,15 +477,9 @@ class ProductService extends BaseService {
|
||||
`To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist. For more info check MEDUSA.com`
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
product.variants.map(async variantId =>
|
||||
this.productVariantService_.deleteOptionValue(variantId, optionId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return this.productModel_.updateOne(
|
||||
const result = await this.productModel_.updateOne(
|
||||
{ _id: productId },
|
||||
{
|
||||
$pull: {
|
||||
@@ -495,6 +489,17 @@ class ProductService extends BaseService {
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// If we reached this point, we can delete option value from variants
|
||||
if (product.variants) {
|
||||
await Promise.all(
|
||||
product.variants.map(async variantId =>
|
||||
this.productVariantService_.deleteOptionValue(variantId, optionId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,7 +548,9 @@ class ProductService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets metadata for a product
|
||||
* Dedicated method to set metadata for a product.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} productId - the product to decorate.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
Arguments:
|
||||
/Users/srindom/.nvm/versions/node/v10.15.3/bin/node /usr/local/Cellar/yarn/1.19.0/libexec/bin/yarn.js add -D client-sessions
|
||||
/usr/local/bin/node /usr/local/Cellar/yarn/1.17.3/libexec/bin/yarn.js
|
||||
|
||||
PATH:
|
||||
/Users/srindom/.rvm/gems/ruby-2.6.3/bin:/Users/srindom/.rvm/gems/ruby-2.6.3@global/bin:/Users/srindom/.rvm/rubies/ruby-2.6.3/bin:/Users/srindom/.nvm/versions/node/v10.15.3/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/TeX/texbin:/Users/srindom/.rvm/bin
|
||||
/Users/oliverjuhl/Desktop/development/google-cloud-sdk/bin:/Library/Frameworks/Python.framework/Versions/3.8/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/oliverjuhl/miniconda3/bin:/Users/oliverjuhl/development/flutter/bin
|
||||
|
||||
Yarn version:
|
||||
1.19.0
|
||||
1.17.3
|
||||
|
||||
Node version:
|
||||
10.15.3
|
||||
10.15.0
|
||||
|
||||
Platform:
|
||||
darwin x64
|
||||
|
||||
Trace:
|
||||
Error: https://registry.yarnpkg.com/medusa-core-utils: Not found
|
||||
at Request.params.callback [as _callback] (/usr/local/Cellar/yarn/1.19.0/libexec/lib/cli.js:66918:18)
|
||||
at Request.self.callback (/usr/local/Cellar/yarn/1.19.0/libexec/lib/cli.js:140539:22)
|
||||
at Request.emit (events.js:189:13)
|
||||
at Request.<anonymous> (/usr/local/Cellar/yarn/1.19.0/libexec/lib/cli.js:141511:10)
|
||||
at Request.emit (events.js:189:13)
|
||||
at IncomingMessage.<anonymous> (/usr/local/Cellar/yarn/1.19.0/libexec/lib/cli.js:141433:12)
|
||||
at Object.onceWrapper (events.js:277:13)
|
||||
at IncomingMessage.emit (events.js:194:15)
|
||||
at endReadableNT (_stream_readable.js:1125:12)
|
||||
at Request.params.callback [as _callback] (/usr/local/Cellar/yarn/1.17.3/libexec/lib/cli.js:66830:18)
|
||||
at Request.self.callback (/usr/local/Cellar/yarn/1.17.3/libexec/lib/cli.js:140464:22)
|
||||
at Request.emit (events.js:182:13)
|
||||
at Request.<anonymous> (/usr/local/Cellar/yarn/1.17.3/libexec/lib/cli.js:141436:10)
|
||||
at Request.emit (events.js:182:13)
|
||||
at IncomingMessage.<anonymous> (/usr/local/Cellar/yarn/1.17.3/libexec/lib/cli.js:141358:12)
|
||||
at Object.onceWrapper (events.js:273:13)
|
||||
at IncomingMessage.emit (events.js:187:15)
|
||||
at endReadableNT (_stream_readable.js:1094:12)
|
||||
at process._tickCallback (internal/process/next_tick.js:63:19)
|
||||
|
||||
npm manifest:
|
||||
@@ -48,6 +48,7 @@ npm manifest:
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"eslint": "^6.7.2",
|
||||
"jest": "^24.9.0",
|
||||
"nodemon": "^2.0.1",
|
||||
@@ -1699,6 +1700,13 @@ Lockfile:
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
||||
|
||||
client-sessions@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/client-sessions/-/client-sessions-0.8.0.tgz#a7d8c5558ad5d56f2a199f3533eb654b5df893fd"
|
||||
integrity sha1-p9jFVYrV1W8qGZ81M+tlS134k/0=
|
||||
dependencies:
|
||||
cookies "^0.7.0"
|
||||
|
||||
cliui@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
|
||||
@@ -1878,6 +1886,14 @@ Lockfile:
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
|
||||
integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==
|
||||
|
||||
cookies@^0.7.0:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa"
|
||||
integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==
|
||||
dependencies:
|
||||
depd "~1.1.2"
|
||||
keygrip "~1.0.3"
|
||||
|
||||
copy-descriptor@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
@@ -3947,6 +3963,11 @@ Lockfile:
|
||||
resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87"
|
||||
integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==
|
||||
|
||||
keygrip@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc"
|
||||
integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==
|
||||
|
||||
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
||||
|
||||
Reference in New Issue
Block a user