Adds admin API endpoints for product operations (#32)

This commit is contained in:
Oliver Windall Juhl
2020-04-08 21:18:03 +02:00
committed by GitHub
parent 904a2369fb
commit 42fdd14e42
27 changed files with 856 additions and 103 deletions

View File

@@ -0,0 +1,43 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("POST /admin/products/:id/options", () => {
describe("successful add option", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/products/${IdMap.getId("productWithOptions")}/options`,
{
payload: {
optionTitle: "Test option",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addOption", () => {
expect(ProductServiceMock.addOption).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.addOption).toHaveBeenCalledWith(
IdMap.getId("productWithOptions"),
"Test option"
)
})
it("returns the updated product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
})
})
})

View File

@@ -0,0 +1,42 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("POST /admin/products/:id/variants/:variantId", () => {
describe("successful add variant", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/products/${IdMap.getId(
"productWithOptions"
)}/variants/${IdMap.getId("variant2")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service addVariant", () => {
expect(ProductServiceMock.addVariant).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.addVariant).toHaveBeenCalledWith(
IdMap.getId("productWithOptions"),
IdMap.getId("variant2")
)
})
it("returns the updated product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
})
})
})

View File

@@ -23,16 +23,12 @@ describe("POST /admin/products", () => {
})
it("returns 200", () => {
expect(subject.status).toEqual(201)
expect(subject.status).toEqual(200)
})
it("returns created product draft", () => {
expect(subject.body).toEqual({
title: "Test Product",
description: "Test Description",
tags: "hi,med,dig",
handle: "test-product",
})
expect(subject.body._id).toEqual(IdMap.getId("product1"))
expect(subject.body.decorated).toEqual(true)
})
it("calls service createDraft", () => {

View File

@@ -0,0 +1,42 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("DELETE /admin/products/:id/options/:optionId", () => {
describe("successfully updates an option", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/products/${IdMap.getId(
"productWithOptions"
)}/options/${IdMap.getId("option1")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200 and correct delete info", () => {
expect(subject.status).toEqual(200)
expect(subject.body).toEqual({
optionId: IdMap.getId("option1"),
object: "option",
deleted: true,
})
})
it("calls update", () => {
expect(ProductServiceMock.deleteOption).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.deleteOption).toHaveBeenCalledWith(
IdMap.getId("productWithOptions"),
IdMap.getId("option1")
)
})
})
})

View File

@@ -0,0 +1,46 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("DELETE /admin/products/:id", () => {
describe("successfully deletes a product", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/products/${IdMap.getId("product1")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls ProductService delete", () => {
expect(ProductServiceMock.delete).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.delete).toHaveBeenCalledWith(
IdMap.getId("product1")
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns correct delete data", () => {
expect(subject.body).toEqual({
id: IdMap.getId("product1"),
object: "product",
deleted: true,
})
})
})
})

View File

@@ -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", () => {
describe("successfully gets a product", () => {
let subject
beforeAll(async () => {
subject = await request(
"GET",
`/admin/products/${IdMap.getId("product1")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls get product from productSerice", () => {
expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("product1")
)
})
it("returns product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("product1"))
expect(subject.body.decorated).toEqual(true)
})
})
})

View File

@@ -0,0 +1,34 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import {
ProductServiceMock,
products,
} from "../../../../../services/__mocks__/product"
describe("GET /admin/products", () => {
describe("successfully lists products", () => {
let subject
beforeAll(async () => {
subject = await request("GET", `/admin/products`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
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)
})
it("calls update", () => {
expect(ProductServiceMock.list).toHaveBeenCalledTimes(1)
})
})
})

View File

@@ -0,0 +1,38 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("POST /admin/products/:id/publish", () => {
describe("successful publish", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/products/${IdMap.getId("publish")}/publish`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns product with published flag true", () => {
expect(subject.body.published).toEqual(true)
})
it("calls service publish", () => {
expect(ProductServiceMock.publish).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.publish).toHaveBeenCalledWith(
IdMap.getId("publish")
)
})
})
})

View File

@@ -0,0 +1,42 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("POST /admin/products/:id/variants/:variantId", () => {
describe("successful removes variant", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/products/${IdMap.getId(
"productWithOptions"
)}/variants/${IdMap.getId("variant1")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service removeVariant", () => {
expect(ProductServiceMock.removeVariant).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.removeVariant).toHaveBeenCalledWith(
IdMap.getId("productWithOptions"),
IdMap.getId("variant1")
)
})
it("returns decorated product with variant removed", () => {
expect(subject.body._id).toEqual(IdMap.getId("productWithOptions"))
expect(subject.body.decorated).toEqual(true)
})
})
})

View File

@@ -0,0 +1,43 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("POST /admin/products/:id/options/:optionId", () => {
describe("successfully updates an option", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/products/${IdMap.getId(
"productWithOptions"
)}/options/${IdMap.getId("option1")}`,
{
payload: {
title: "Updated option title",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls update", () => {
expect(ProductServiceMock.updateOption).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.updateOption).toHaveBeenCalledWith(
IdMap.getId("productWithOptions"),
IdMap.getId("option1"),
{
title: "Updated option title",
}
)
})
})
})

View File

@@ -0,0 +1,90 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("POST /admin/products/:id", () => {
describe("successfully updates a product", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/products/${IdMap.getId("product1")}`,
{
payload: {
title: "Product 1",
description: "Updated test description",
handle: "handle",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls update", () => {
expect(ProductServiceMock.update).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("product1"),
{
title: "Product 1",
description: "Updated test description",
handle: "handle",
}
)
})
})
describe("handles failed update operation", () => {
it("throws if metadata is to be updated", async () => {
try {
await request("POST", `/admin/products/${IdMap.getId("product1")}`, {
payload: {
_id: IdMap.getId("product1"),
title: "Product 1",
metadata: "Test Description",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
} catch (error) {
expect(error.status).toEqual(400)
expect(error.message).toEqual(
"Use setMetadata to update metadata fields"
)
}
})
it("throws if variants is to be updated", async () => {
try {
await request("POST", `/admin/products/${IdMap.getId("product1")}`, {
payload: {
_id: IdMap.getId("product1"),
title: "Product 1",
metadata: "Test Description",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
} catch (error) {
expect(error.status).toEqual(400)
expect(error.message).toEqual(
"Use addVariant, reorderVariants, removeVariant to update Product Variants"
)
}
})
})
})

View File

@@ -0,0 +1,34 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
optionTitle: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productService = req.scope.resolve("productService")
const product = await productService.retrieve(id)
await productService.addOption(product._id, value.optionTitle)
let newProduct = await productService.retrieve(product._id)
newProduct = await productService.decorate(newProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(newProduct)
} catch (err) {
console.log(err)
throw err
}
}

View File

@@ -0,0 +1,23 @@
export default async (req, res) => {
const { id, variantId } = req.params
try {
const productService = req.scope.resolve("productService")
const product = await productService.retrieve(id)
await productService.addVariant(product._id, variantId)
let newProduct = await productService.retrieve(product._id)
newProduct = await productService.decorate(newProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(newProduct)
} catch (err) {
throw err
}
}

View File

@@ -18,10 +18,18 @@ export default async (req, res) => {
try {
const productService = req.scope.resolve("productService")
const data = await productService.createDraft(value)
// Return the created product draft
res.status(201).json(data)
let newProduct = await productService.createDraft(value)
newProduct = await productService.decorate(newProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(newProduct)
} catch (err) {
throw err
}

View File

@@ -0,0 +1,15 @@
export default async (req, res) => {
const { id, optionId } = req.params
try {
const productService = req.scope.resolve("productService")
await productService.deleteOption(id, optionId)
res.json({
optionId,
object: "option",
deleted: true,
})
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,15 @@
export default async (req, res) => {
const { id } = req.params
try {
const productService = req.scope.resolve("productService")
await productService.delete(id)
res.json({
id,
object: "product",
deleted: true,
})
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,19 @@
export default async (req, res) => {
const { id } = req.params
const productService = req.scope.resolve("productService")
let product = await productService.retrieve(id)
product = await productService.decorate(product, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(product)
}

View File

@@ -7,8 +7,33 @@ export default app => {
app.use("/products", route)
route.post("/", middlewares.wrap(require("./create-product").default))
route.post("/:id", middlewares.wrap(require("./update-product").default))
route.post(
"/:id/publish",
middlewares.wrap(require("./publish-product").default)
)
route.post(
"/:id/variants/:variantId",
middlewares.wrap(require("./add-variant").default)
)
route.post(
"/:id/options/:optionId",
middlewares.wrap(require("./update-option").default)
)
route.post("/:id/options", middlewares.wrap(require("./add-option").default))
// route.get("/:productId", middlewares.wrap(require("./get-product").default))
route.delete(
"/:id/variants/:variantId",
middlewares.wrap(require("./remove-variant").default)
)
route.delete("/:id", middlewares.wrap(require("./delete-product").default))
route.delete(
"/:id/options/:optionId",
middlewares.wrap(require("./delete-option").default)
)
route.get("/:id", middlewares.wrap(require("./get-product").default))
route.get("/", middlewares.wrap(require("./list-products").default))
return app
}

View File

@@ -0,0 +1,24 @@
export default async (req, res) => {
try {
const productService = req.scope.resolve("productService")
let products = await productService.list({})
products = await Promise.all(
products.map(
async product =>
await productService.decorate(product, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
)
)
res.json(products)
} catch (error) {
throw error
}
}

View File

@@ -0,0 +1,23 @@
export default async (req, res) => {
const { id } = req.params
try {
const productService = req.scope.resolve("productService")
const product = await productService.retrieve(id)
await productService.publish(product._id)
let publishedProduct = await productService.retrieve(product._id)
publishedProduct = await productService.decorate(publishedProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(publishedProduct)
} catch (error) {
throw error
}
}

View File

@@ -0,0 +1,22 @@
export default async (req, res) => {
const { id, variantId } = req.params
try {
const productService = req.scope.resolve("productService")
await productService.removeVariant(id, variantId)
let updatedProduct = await productService.retrieve(id)
updatedProduct = await productService.decorate(updatedProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(updatedProduct)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,38 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id, optionId } = req.params
const schema = Validator.object().keys({
title: Validator.string(),
values: Validator.array().items(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productService = req.scope.resolve("productService")
const product = await productService.retrieve(id)
await productService.updateOption(product._id, optionId, value)
let newProduct = await productService.retrieve(product._id)
newProduct = await productService.decorate(newProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(newProduct)
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,44 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
title: Validator.string().required(),
description: Validator.string().optional(),
tags: Validator.string().optional(),
handle: Validator.string().required(),
images: Validator.array()
.items(Validator.string())
.optional(),
variants: Validator.array()
.items(Validator.string())
.optional(),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const productService = req.scope.resolve("productService")
const oldProduct = await productService.retrieve(id)
await productService.update(oldProduct._id, value)
let newProduct = await productService.retrieve(oldProduct._id)
newProduct = await productService.decorate(newProduct, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(newProduct)
} catch (err) {
throw err
}
}

View File

@@ -1,40 +1,24 @@
import mongoose from "mongoose"
import getProduct from "../get-product"
import { request } from "../../../../../helpers/test-request"
import { IdMap } from "medusa-test-utils"
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
describe("Get product by id", () => {
const testId = `${mongoose.Types.ObjectId("56cb91bdc3464f14678934ca")}`
const productServiceMock = {
retrieve: jest.fn().mockImplementation(id => {
if (id === testId) {
return Promise.resolve({ _id: id, title: "test" })
}
return Promise.resolve(undefined)
}),
}
const reqMock = id => {
return {
params: {
productId: id,
},
scope: {
resolve: jest.fn().mockImplementation(name => {
if (name === "productService") {
return productServiceMock
}
return undefined
}),
},
}
}
const resMock = {
sendStatus: jest.fn().mockReturnValue(),
json: jest.fn().mockReturnValue(),
}
describe("get product by id successfull", () => {
let subject
beforeAll(async () => {
await getProduct(reqMock(testId), resMock)
subject = await request(
"GET",
`/admin/products/${IdMap.getId("product1")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
@@ -42,52 +26,15 @@ describe("Get product by id", () => {
})
it("calls get product from productSerice", () => {
expect(productServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(productServiceMock.retrieve).toHaveBeenCalledWith(testId)
expect(ProductServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(ProductServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("product1")
)
})
it("calls res.json", () => {
expect(resMock.json).toHaveBeenCalledTimes(1)
expect(resMock.json).toHaveBeenCalledWith({
_id: testId,
title: "test",
})
})
})
describe("returns 404 when product not found", () => {
beforeAll(async () => {
const id = mongoose.Types.ObjectId()
await getProduct(reqMock(`${id}`), resMock)
})
afterAll(() => {
jest.clearAllMocks()
})
it("return 404", () => {
expect(resMock.sendStatus).toHaveBeenCalledTimes(1)
expect(resMock.json).toHaveBeenCalledTimes(0)
expect(resMock.sendStatus).toHaveBeenCalledWith(404)
})
})
describe("fails when validation fails", () => {
let res
beforeAll(async () => {
try {
await getProduct(reqMock(`not object id`), resMock)
} catch (err) {
res = err
}
})
afterAll(() => {
jest.clearAllMocks()
})
it("return 404", () => {
expect(res.name).toEqual("ValidationError")
it("returns product decorated", () => {
expect(subject.body._id).toEqual(IdMap.getId("product1"))
expect(subject.body.decorated).toEqual(true)
})
})
})

View File

@@ -11,12 +11,18 @@ export default async (req, res) => {
}
const productService = req.scope.resolve("productService")
const product = await productService.retrieve(value)
let product = await productService.retrieve(value)
if (!product) {
res.sendStatus(404)
return
}
product = await productService.decorate(product, [
"title",
"description",
"tags",
"handle",
"images",
"options",
"variants",
"published",
])
res.json(product)
}

View File

@@ -1,32 +1,84 @@
import { IdMap } from "medusa-test-utils"
import { MedusaError } from "medusa-core-utils"
export const products = {
product1: {
_id: IdMap.getId("product1"),
name: "Product 1",
title: "Product 1",
},
publishProduct: {
_id: IdMap.getId("publish"),
title: "Product 1",
published: true,
},
product2: {
_id: IdMap.getId("product2"),
name: "Product 2",
title: "Product 2",
},
productWithOptions: {
_id: IdMap.getId("productWithOptions"),
title: "Test",
variants: [IdMap.getId("variant1")],
options: [
{
_id: IdMap.getId("option1"),
title: "Test",
values: [IdMap.getId("optionValue1")],
},
],
},
}
export const ProductServiceMock = {
createDraft: jest.fn().mockImplementation(data => {
return Promise.resolve(data)
return Promise.resolve(products.product1)
}),
retrieve: jest.fn().mockImplementation(id => {
if (id === IdMap.getId("validId")) {
return Promise.resolve({ _id: IdMap.getId("validId") })
}
if (id === IdMap.getId("product1")) {
publish: jest.fn().mockImplementation(_ => {
return Promise.resolve({
_id: IdMap.getId("publish"),
name: "Product 1",
published: true,
})
}),
delete: jest.fn().mockImplementation(_ => {
return Promise.resolve()
}),
addVariant: jest.fn().mockImplementation((productId, variantId) => {
return Promise.resolve(products.productWithOptions)
}),
removeVariant: jest.fn().mockImplementation((productId, variantId) => {
return Promise.resolve(products.productWithOptions)
}),
decorate: jest.fn().mockImplementation((product, fields) => {
product.decorated = true
return product
}),
addOption: jest.fn().mockImplementation((productId, optionTitle) => {
return Promise.resolve(products.productWithOptions)
}),
updateOption: jest.fn().mockReturnValue(Promise.resolve()),
deleteOption: jest.fn().mockReturnValue(Promise.resolve()),
retrieve: jest.fn().mockImplementation(productId => {
if (productId === IdMap.getId("product1")) {
return Promise.resolve(products.product1)
}
if (id === IdMap.getId("product2")) {
if (productId === IdMap.getId("product2")) {
return Promise.resolve(products.product2)
}
if (productId === IdMap.getId("validId")) {
return Promise.resolve({ _id: IdMap.getId("validId") })
}
if (productId === IdMap.getId("publish")) {
return Promise.resolve(products.publishProduct)
}
if (productId === IdMap.getId("productWithOptions")) {
return Promise.resolve(products.productWithOptions)
}
return Promise.resolve(undefined)
}),
update: jest.fn().mockImplementation((userId, data) => {
return Promise.resolve()
}),
list: jest.fn().mockImplementation(data => {
// Used to retrieve a product based on a variant id see
// ProductVariantService.addOptionValue
@@ -56,7 +108,10 @@ export const ProductServiceMock = {
return Promise.resolve([])
}
return Promise.resolve([products.product1, products.product2])
return Promise.resolve([
{ ...products.product1, decorated: true },
{ ...products.product2, decorated: true },
])
}),
}

View File

@@ -379,7 +379,7 @@ class ProductService extends BaseService {
)
}
const { title } = data
const { title, values } = data
const titleExists = product.options.some(
o => o.title.toUpperCase() === title.toUpperCase()
)