Updates API endpoints to match new product-to-variant structure

This commit is contained in:
Sebastian Rindom
2020-05-26 22:02:40 +02:00
parent 3183efa94f
commit 0bcff435a3
32 changed files with 255 additions and 99 deletions
@@ -16,6 +16,7 @@ export default () => {
break
case MedusaError.Types.DB_ERROR:
statusCode = 500
logger.error(err)
break
default:
break
@@ -36,8 +36,10 @@ describe("POST /admin/products/:id/options", () => {
})
it("returns the updated product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(
IdMap.getId("productWithOptions")
)
expect(subject.body.product.decorated).toEqual(true)
})
})
})
@@ -27,8 +27,8 @@ describe("POST /admin/products", () => {
})
it("returns created product draft", () => {
expect(subject.body._id).toEqual(IdMap.getId("product1"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(IdMap.getId("product1"))
expect(subject.body.product.decorated).toEqual(true)
})
it("calls service createDraft", () => {
@@ -39,6 +39,7 @@ describe("POST /admin/products/:id/variants", () => {
IdMap.getId("productWithOptions"),
{
title: "Test Product Variant",
options: [],
prices: [
{
currency_code: "DKK",
@@ -50,8 +51,10 @@ describe("POST /admin/products/:id/variants", () => {
})
it("returns the updated product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(
IdMap.getId("productWithOptions")
)
expect(subject.body.product.decorated).toEqual(true)
})
})
})
@@ -25,7 +25,7 @@ describe("DELETE /admin/products/:id/options/:optionId", () => {
it("returns 200 and correct delete info", () => {
expect(subject.status).toEqual(200)
expect(subject.body).toEqual({
optionId: IdMap.getId("option1"),
option_id: IdMap.getId("option1"),
object: "option",
deleted: true,
})
@@ -34,9 +34,12 @@ describe("POST /admin/products/:id/variants/:variantId", () => {
)
})
it("returns decorated product with variant removed", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
it("returns delete result", () => {
expect(subject.body).toEqual({
variant_id: IdMap.getId("variant1"),
object: "product-variant",
deleted: true,
})
})
})
})
@@ -32,8 +32,8 @@ describe("GET /admin/products/:id", () => {
})
it("returns product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("product1"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(IdMap.getId("product1"))
expect(subject.body.product.decorated).toEqual(true)
})
})
})
@@ -0,0 +1,39 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("GET /admin/products/:id/variants", () => {
describe("successfully gets a product", () => {
let subject
beforeAll(async () => {
subject = await request(
"GET",
`/admin/products/${IdMap.getId("product1")}/variants`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls get product from productSerice", () => {
expect(ProductServiceMock.retrieveVariants).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.retrieveVariants).toHaveBeenCalledWith(
IdMap.getId("product1")
)
})
it("returns variants", () => {
expect(subject.body.variants[0]._id).toEqual(IdMap.getId("1"))
expect(subject.body.variants[1]._id).toEqual(IdMap.getId("2"))
})
})
})
@@ -21,10 +21,10 @@ describe("GET /admin/products", () => {
it("returns 200 and decorated products", () => {
expect(subject.status).toEqual(200)
expect(subject.body[0]._id).toEqual(products.product1._id)
expect(subject.body[0].decorated).toEqual(true)
expect(subject.body[1]._id).toEqual(products.product2._id)
expect(subject.body[1].decorated).toEqual(true)
expect(subject.body.products[0]._id).toEqual(products.product1._id)
expect(subject.body.products[0].decorated).toEqual(true)
expect(subject.body.products[1]._id).toEqual(products.product2._id)
expect(subject.body.products[1].decorated).toEqual(true)
})
it("calls update", () => {
@@ -25,7 +25,7 @@ describe("POST /admin/products/:id/publish", () => {
})
it("returns product with published flag true", () => {
expect(subject.body.published).toEqual(true)
expect(subject.body.product.published).toEqual(true)
})
it("calls service publish", () => {
@@ -70,8 +70,10 @@ describe("POST /admin/products/:id/variants/:variantId", () => {
})
it("returns decorated product with variant removed", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(
IdMap.getId("productWithOptions")
)
expect(subject.body.product.decorated).toEqual(true)
})
})
@@ -118,8 +120,10 @@ describe("POST /admin/products/:id/variants/:variantId", () => {
})
it("returns decorated product with variant removed", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(
IdMap.getId("productWithOptions")
)
expect(subject.body.product.decorated).toEqual(true)
})
})
@@ -165,8 +169,10 @@ describe("POST /admin/products/:id/variants/:variantId", () => {
})
it("returns decorated product with variant removed", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(
IdMap.getId("productWithOptions")
)
expect(subject.body.product.decorated).toEqual(true)
})
})
})
@@ -25,7 +25,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(data)
res.json({ product: data })
} catch (err) {
throw err
}
@@ -29,7 +29,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(newProduct)
res.json({ product: newProduct })
} catch (err) {
throw err
}
@@ -10,10 +10,12 @@ export default async (req, res) => {
amount: Validator.number().required(),
})
.required(),
options: Validator.array().items({
option_id: Validator.objectId().required(),
value: Validator.string().required(),
}),
options: Validator.array()
.items({
option_id: Validator.objectId().required(),
value: Validator.string().required(),
})
.default([]),
image: Validator.string().optional(),
inventory_quantity: Validator.number().optional(),
allow_backorder: Validator.boolean().optional(),
@@ -39,7 +41,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(data)
res.json({ product: data })
} catch (err) {
throw err
}
@@ -1,11 +1,11 @@
export default async (req, res) => {
const { id, optionId } = req.params
const { id, option_id } = req.params
try {
const productService = req.scope.resolve("productService")
await productService.deleteOption(id, optionId)
await productService.deleteOption(id, option_id)
res.json({
optionId,
option_id,
object: "option",
deleted: true,
})
@@ -1,9 +1,9 @@
export default async (req, res) => {
const { id, variantId } = req.params
const { id, variant_id } = req.params
try {
const productService = req.scope.resolve("productService")
const product = await productService.deleteVariant(id, variantId)
const product = await productService.deleteVariant(id, variant_id)
const data = await productService.decorate(product, [
"title",
"description",
@@ -14,7 +14,12 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(product)
res.json({
variant_id,
object: "product-variant",
deleted: true,
})
} catch (err) {
throw err
}
@@ -15,5 +15,5 @@ export default async (req, res) => {
"published",
])
res.json(product)
res.json({ product })
}
@@ -0,0 +1,8 @@
export default async (req, res) => {
const { id } = req.params
const productService = req.scope.resolve("productService")
const variants = await productService.retrieveVariants(id)
res.json({ variants })
}
@@ -18,24 +18,29 @@ export default app => {
middlewares.wrap(require("./create-variant").default)
)
route.get(
"/:id/variants",
middlewares.wrap(require("./get-variants").default)
)
route.post(
"/:id/variants/:variant_id",
middlewares.wrap(require("./update-variant").default)
)
route.post(
"/:id/options/:optionId",
"/:id/options/:option_id",
middlewares.wrap(require("./update-option").default)
)
route.post("/:id/options", middlewares.wrap(require("./add-option").default))
route.delete(
"/:id/variants/:variantId",
"/:id/variants/:variant_id",
middlewares.wrap(require("./delete-variant").default)
)
route.delete("/:id", middlewares.wrap(require("./delete-product").default))
route.delete(
"/:id/options/:optionId",
"/:id/options/:option_id",
middlewares.wrap(require("./delete-option").default)
)
@@ -17,7 +17,7 @@ export default async (req, res) => {
])
)
)
res.json(products)
res.json({ products })
} catch (error) {
throw error
}
@@ -16,7 +16,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(publishedProduct)
res.json({ product: publishedProduct })
} catch (error) {
throw error
}
@@ -1,11 +1,10 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id, optionId } = req.params
const { id, option_id } = req.params
const schema = Validator.object().keys({
title: Validator.string(),
values: Validator.array().items(),
})
const { value, error } = schema.validate(req.body)
@@ -15,23 +14,25 @@ export default async (req, res) => {
try {
const productService = req.scope.resolve("productService")
const product = await productService.retrieve(id)
await productService.updateOption(product._id, optionId, value)
const product = await productService.updateOption(id, option_id, value)
let newProduct = await productService.retrieve(product._id)
newProduct = await productService.decorate(newProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
const data = await productService.decorate(
product,
[
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
],
["variants"]
)
res.json(newProduct)
res.json({ product: data })
} catch (err) {
throw err
}
@@ -37,7 +37,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(newProduct)
res.json({ product: newProduct })
} catch (err) {
throw err
}
@@ -84,7 +84,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(data)
res.json({ product: data })
} catch (err) {
throw err
}
@@ -33,8 +33,8 @@ describe("Get product by id", () => {
})
it("returns product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("product1"))
expect(subject.body.decorated).toEqual(true)
expect(subject.body.product._id).toEqual(IdMap.getId("product1"))
expect(subject.body.product.decorated).toEqual(true)
})
})
})
@@ -13,16 +13,20 @@ export default async (req, res) => {
const productService = req.scope.resolve("productService")
let product = await productService.retrieve(value)
product = await productService.decorate(product, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
product = await productService.decorate(
product,
[
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
],
["variants"]
)
res.json(product)
}
@@ -6,5 +6,24 @@ export default async (req, res) => {
const productService = req.scope.resolve("productService")
const products = await productService.list(selector)
res.json(products)
const data = await Promise.all(
products.map(p =>
productService.decorate(
p,
[
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
],
["variants"]
)
)
)
res.json(data)
}
@@ -10,6 +10,13 @@ export const ProductModelMock = {
}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query._id === IdMap.getId("fakeId")) {
return Promise.resolve({
_id: IdMap.getId("fakeId"),
title: "Product With Variants",
variants: ["1", "2", "3"],
})
}
if (query._id === IdMap.getId("productWithFourVariants")) {
return Promise.resolve({
_id: IdMap.getId("productWithFourVariants"),
@@ -56,9 +56,16 @@ export const ProductServiceMock = {
addOption: jest.fn().mockImplementation((productId, optionTitle) => {
return Promise.resolve(products.productWithOptions)
}),
updateOption: jest.fn().mockReturnValue(Promise.resolve()),
updateOption: jest
.fn()
.mockReturnValue(Promise.resolve(products.productWithOptions)),
updateOptionValue: jest.fn().mockReturnValue(Promise.resolve()),
deleteOption: jest.fn().mockReturnValue(Promise.resolve()),
retrieveVariants: jest
.fn()
.mockReturnValue(
Promise.resolve([{ _id: IdMap.getId("1") }, { _id: IdMap.getId("2") }])
),
retrieve: jest.fn().mockImplementation(productId => {
if (productId === IdMap.getId("product1")) {
return Promise.resolve(products.product1)
@@ -130,7 +130,7 @@ describe("ProductService", () => {
})
const fakeProduct = {
_id: "1234",
_id: IdMap.getId("fakeId"),
variants: ["1", "2", "3"],
tags: "testtag1, testtag2",
handle: "test-product",
@@ -148,7 +148,7 @@ describe("ProductService", () => {
["variants"]
)
expect(decorated).toEqual({
_id: "1234",
_id: IdMap.getId("fakeId"),
metadata: { testKey: "testValue" },
variants: [variants.one, variants.two, variants.three],
})
@@ -161,7 +161,7 @@ describe("ProductService", () => {
["variants"]
)
expect(decorated).toEqual({
_id: "1234",
_id: IdMap.getId("fakeId"),
metadata: { testKey: "testValue" },
handle: "test-product",
variants: [variants.one, variants.two, variants.three],
@@ -174,7 +174,7 @@ describe("ProductService", () => {
"tags",
])
expect(decorated).toEqual({
_id: "1234",
_id: IdMap.getId("fakeId"),
metadata: { testKey: "testValue" },
tags: "testtag1, testtag2",
handle: "test-product",
@@ -184,7 +184,7 @@ describe("ProductService", () => {
it("returns decorated product with metadata", async () => {
const decorated = await productService.decorate(fakeProduct, [])
expect(decorated).toEqual({
_id: "1234",
_id: IdMap.getId("fakeId"),
metadata: { testKey: "testValue" },
})
})
@@ -316,7 +316,7 @@ describe("ProductService", () => {
productVariantService: ProductVariantServiceMock,
})
beforeEach(() => {
afterEach(() => {
jest.clearAllMocks()
})
@@ -350,7 +350,7 @@ describe("ProductService", () => {
],
})
expect(ProductModelMock.findOne).toBeCalledTimes(1)
expect(ProductModelMock.findOne).toBeCalledTimes(2)
expect(ProductModelMock.findOne).toBeCalledWith({
_id: IdMap.getId("variantProductId"),
})
@@ -362,6 +362,51 @@ describe("ProductService", () => {
)
})
it("add variant to product successfully", async () => {
await productService.createVariant(
IdMap.getId("productWithFourVariants"),
{
title: "variant1",
options: [
{
option_id: IdMap.getId("color_id"),
value: "blue",
},
{
option_id: IdMap.getId("size_id"),
value: "1600",
},
],
}
)
expect(ProductVariantServiceMock.createDraft).toBeCalledTimes(1)
expect(ProductVariantServiceMock.createDraft).toBeCalledWith({
title: "variant1",
options: [
{
option_id: IdMap.getId("color_id"),
value: "blue",
},
{
option_id: IdMap.getId("size_id"),
value: "1600",
},
],
})
expect(ProductModelMock.findOne).toBeCalledTimes(2)
expect(ProductModelMock.findOne).toBeCalledWith({
_id: IdMap.getId("productWithFourVariants"),
})
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{ _id: IdMap.getId("productWithFourVariants") },
{ $push: { variants: expect.stringMatching(/.*/) } }
)
})
it("throws error if option id is not present in product", async () => {
await expect(
productService.createVariant(IdMap.getId("variantProductId"), {
@@ -314,8 +314,8 @@ class ProductVariantService extends BaseService {
}
return this.productVariantModel_.updateOne(
{ _id: variant._id, "options.option_id": optionId },
{ $set: { "options.$.option_id": `${optionValue}` } }
{ _id: variantId, "options.option_id": optionId },
{ $set: { "options.$.value": `${optionValue}` } }
)
}
+15 -16
View File
@@ -202,14 +202,17 @@ class ProductService extends BaseService {
let combinationExists = false
if (product.variants && product.variants.length) {
const variants = await this.retrieveVariants(productId)
// Check if option value of the variant to add already exists. Go through
// each existing variant. Check if this variants option values are
// identical to the option values of the variant being added.
combinationExists = product.variants.some(async vId => {
const v = await this.productVariantService_.retrieve(vId)
return v.options.reduce((acc, option, index) => {
return acc && option.value === variant.options[index].value
}, true)
combinationExists = variants.some(v => {
return v.options.every(option => {
const variantOption = variant.options.find(o =>
option.option_id.equals(o.option_id)
)
return option.value === variantOption.value
})
})
}
@@ -440,14 +443,14 @@ class ProductService extends BaseService {
const firstVariant = await this.productVariantService_.retrieve(
product.variants[0]
)
const valueToMatch = firstVariant.options.find(
o => o.option_id === optionId
const valueToMatch = firstVariant.options.find(o =>
o.option_id.equals(optionId)
).value
const equalsFirst = await Promise.all(
product.variants.map(async vId => {
const v = await this.productVariantService_.retrieve(vId)
const option = v.options.find(o => o.option_id === optionId)
const option = v.options.find(o => o.option_id.equals(optionId))
return option.value === valueToMatch
})
)
@@ -529,12 +532,12 @@ class ProductService extends BaseService {
// Check if the variant's options are identical to the variant we
// are updating
const hasMatchingOptions = v.options.every(option => {
if (option.option_id === optionId) {
if (option.option_id.equals(optionId)) {
return option.value === value
}
const toUpdateOption = toUpdate.options.find(
o => o.option_id === option.option_id
const toUpdateOption = toUpdate.options.find(o =>
option.option_id.equals(o.option_id)
)
return toUpdateOption.value === option.value
})
@@ -567,11 +570,7 @@ class ProductService extends BaseService {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(product, fields.concat(requiredFields))
if (expandFields.includes("variants")) {
decorated.variants = await Promise.all(
product.variants.map(variantId =>
this.productVariantService_.retrieve(variantId)
)
)
decorated.variants = await this.retrieveVariants(product._id)
}
return decorated
}