feat(types, product): added product module update (#4504)
* Feat: create product with product module * feat: create product wip * feat: create product wip * feat: update product relation and generate image migration * lint * conitnue implementation * continue implementation and add integration tests for produceService.create * Add integration tests for product creation at the module level for the complete flow * only use persist since write operations are always wrapped in a transaction which will be committed and flushed * simplify the transaction wrapper to make future changes easier * feat: move some utils to the utils package to simplify its usage * tests: fix unit tests * feat: create variants along side the product * Add more integration tests an update migrations * chore: Update actions workflow to include packages integration tests * small types and utils cleanup * chore: Add support for database debug option * chore: Add missing types in package.json from types and util, validate that all the models are sync with medusa * expose retrieve method * fix types issues * fix unit tests and move integration tests workflow with the plugins integration tests * chore: remove migration function export from the definition to prevent them to be ran by the medusa cli just in case * fix package.json script * chore: workflows * feat: start creating the create product workflow * feat: add empty step for prices and sales channel * tests: update scripts and action envs * fix imports * feat: Add proper soft deleted support + add product deletion service public api * chore: update migrations * chore: update migrations * chore: update todo * feat: Add product deletion to the create-product workflow as compensation * chore: cleanup product utils * feat: Add support for cascade soft-remove * feat: refactor repository to take into account withDeleted * fix integration tests * Add support for force delete -> delete, cleanup repositories and improvements * Add support for restoring a product and add integration tests * cleaup + tests * types * fix integration tests * remove unnecessary comments * move specific mikro orm usage to the DAL * Cleanup workflow functions * Make deleted_at optional at the property level and add url index for the images * address feedback + cleanup * fix export * merge migrations into one * feat(product, types): added missing product variant methods (#4475) * chore: added missing product variant methods * chore: address PR feedback * chore: catch undefined case for retrieve + specs for variant service * chore: align TEntity + add changeset * chore: revert changeset, TEntity to ProductVariant * chore: write tests for pagination, unskip the test * Create chilled-mice-deliver.md * update integration fixtuers * update pipeline node version * rename github action * fix pipeline * feat(medusa, types): added missing category tests and service methods (#4499) * chore: added missing category tests and service methods * chore: added type changes to module service * chore: address pr feedback * chore: added product module update * chore: use status enum type from common types * chore: remove flushing at repo level, pass in relation instead of ID * chore: update error message for missing id * update repositories manager usage and serialisation from the write public API * move serializisation to the DAL * rename template args * chore: address feedback * chore: wip * chore: added collection methods for module and collection service (#4505) * chore: added collection methods for module and collection service * Create fresh-islands-teach.md * chore: move retrieve entity to utils package * chore: make products optional in DTO type --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * chore: added categories, collections and other relations to update * feat(product): Apply transaction decorators to the services (#4512) * chore: handle variant update, create and delete through products update * chore: cleanup types, self review * chore: remove relations that are not present in collection * chore: address reviews p1 * chore: add test for incorrect ID + remove extra check on variant id existance * chore: cleanup + add changeset * chore: wip * chore: add todos for getter method --------- Co-authored-by: adrien2p <adrien.deperetti@gmail.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Oliver Windall Juhl
adrien2p
Carlos R. L. Rodrigues
parent
585ebf2454
commit
caea44ebfd
+464
@@ -0,0 +1,464 @@
|
||||
import { MedusaModule } from "@medusajs/modules-sdk"
|
||||
import { Product, ProductCategory, ProductCollection, ProductType, ProductVariant } from "@models"
|
||||
import { IProductModuleService, ProductTypes } from "@medusajs/types"
|
||||
|
||||
import { initialize } from "../../../../src"
|
||||
import { DB_URL, TestDatabase } from "../../../utils"
|
||||
import { buildProductAndRelationsData } from "../../../__fixtures__/product/data/create-product"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import { createCollections, createTypes } from "../../../__fixtures__/product"
|
||||
|
||||
const beforeEach_ = async () => {
|
||||
await TestDatabase.setupDatabase()
|
||||
return await TestDatabase.forkManager()
|
||||
}
|
||||
|
||||
const afterEach_ = async () => {
|
||||
await TestDatabase.clearDatabase()
|
||||
}
|
||||
|
||||
describe("ProductModuleService products", function () {
|
||||
describe("update", function () {
|
||||
let module: IProductModuleService
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
let productCategoryOne: ProductCategory
|
||||
let productCategoryTwo: ProductCategory
|
||||
let productCollectionOne: ProductCollection
|
||||
let productCollectionTwo: ProductCollection
|
||||
let variantOne: ProductVariant
|
||||
let variantTwo: ProductVariant
|
||||
let variantThree: ProductVariant
|
||||
let productTypeOne: ProductType
|
||||
let productTypeTwo: ProductType
|
||||
let images = ["image-1"]
|
||||
|
||||
const productCategoriesData = [{
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
}, {
|
||||
id: "test-2",
|
||||
name: "category 2",
|
||||
}]
|
||||
|
||||
const productCollectionsData = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
},
|
||||
]
|
||||
|
||||
const productTypesData = [
|
||||
{
|
||||
id: "type-1",
|
||||
value: "type 1",
|
||||
},
|
||||
{
|
||||
id: "type-2",
|
||||
value: "type 2",
|
||||
},
|
||||
]
|
||||
|
||||
const tagsData = [{
|
||||
id: "tag-1",
|
||||
value: "tag 1",
|
||||
}]
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await beforeEach_()
|
||||
|
||||
const collections = await createCollections(
|
||||
testManager,
|
||||
productCollectionsData
|
||||
)
|
||||
|
||||
productCollectionOne = collections[0]
|
||||
productCollectionTwo = collections[1]
|
||||
|
||||
const types = await createTypes(
|
||||
testManager,
|
||||
productTypesData,
|
||||
)
|
||||
|
||||
productTypeOne = types[0]
|
||||
productTypeTwo = types[1]
|
||||
|
||||
const categories = (await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesData
|
||||
))
|
||||
|
||||
productCategoryOne = categories[0]
|
||||
productCategoryTwo = categories[1]
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
productTwo = testManager.create(Product, {
|
||||
id: "product-2",
|
||||
title: "product 2",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
categories: [productCategoryOne],
|
||||
collection_id: productCollectionOne.id,
|
||||
tags: tagsData,
|
||||
})
|
||||
|
||||
variantOne = testManager.create(ProductVariant, {
|
||||
id: "variant-1",
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
variantTwo = testManager.create(ProductVariant, {
|
||||
id: "variant-2",
|
||||
title: "variant 2",
|
||||
inventory_quantity: 10,
|
||||
product: productTwo,
|
||||
})
|
||||
|
||||
variantThree = testManager.create(ProductVariant, {
|
||||
id: "variant-3",
|
||||
title: "variant 3",
|
||||
inventory_quantity: 10,
|
||||
product: productTwo,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([productOne, productTwo])
|
||||
|
||||
MedusaModule.clearInstances()
|
||||
|
||||
module = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(afterEach_)
|
||||
|
||||
it("should update a product and upsert relations that are not created yet", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0],
|
||||
})
|
||||
|
||||
const updateData = {
|
||||
...data,
|
||||
id: productOne.id,
|
||||
title: "updated title"
|
||||
}
|
||||
|
||||
const updatedProducts = await module.update([updateData])
|
||||
expect(updatedProducts).toHaveLength(1)
|
||||
|
||||
const product = await module.retrieve(updateData.id, {
|
||||
relations: ["images", "variants", "options", "options.values", "variants.options", "tags", "type",]
|
||||
})
|
||||
|
||||
expect(product.images).toHaveLength(1)
|
||||
expect(product.variants[0].options).toHaveLength(1)
|
||||
expect(product.tags).toHaveLength(1)
|
||||
expect(product.variants).toHaveLength(1)
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "updated title",
|
||||
description: updateData.description,
|
||||
subtitle: updateData.subtitle,
|
||||
is_giftcard: updateData.is_giftcard,
|
||||
discountable: updateData.discountable,
|
||||
thumbnail: images[0],
|
||||
status: updateData.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
url: images[0],
|
||||
}),
|
||||
]),
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: updateData.options[0].title,
|
||||
values: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: updateData.variants[0].options?.[0].value,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
tags: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: updateData.tags[0].value,
|
||||
}),
|
||||
]),
|
||||
type: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: updateData.type.value,
|
||||
}),
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: updateData.variants[0].title,
|
||||
sku: updateData.variants[0].sku,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
inventory_quantity: "100",
|
||||
variant_rank: "0",
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: updateData.variants[0].options?.[0].value,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should add relationships to a product", async () => {
|
||||
const updateData = {
|
||||
id: productOne.id,
|
||||
categories: [{
|
||||
id: productCategoryOne.id
|
||||
}],
|
||||
collection_id: productCollectionOne.id,
|
||||
type_id: productTypeOne.id
|
||||
}
|
||||
|
||||
await module.update([updateData])
|
||||
|
||||
const product = await module.retrieve(updateData.id, {
|
||||
relations: ["categories", "collection", "type"]
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
categories: [
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id
|
||||
})
|
||||
],
|
||||
collection: expect.objectContaining({
|
||||
id: productCollectionOne.id
|
||||
}),
|
||||
type: expect.objectContaining({
|
||||
id: productTypeOne.id
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should upsert a product type when type object is passed", async () => {
|
||||
let updateData = {
|
||||
id: productTwo.id,
|
||||
type: {
|
||||
id: productTypeOne.id,
|
||||
value: productTypeOne.value
|
||||
}
|
||||
}
|
||||
|
||||
await module.update([updateData])
|
||||
|
||||
let product = await module.retrieve(updateData.id, {
|
||||
relations: ["type"]
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
type: expect.objectContaining({
|
||||
id: productTypeOne.id
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
updateData = {
|
||||
id: productTwo.id,
|
||||
type: {
|
||||
id: "new-type-id",
|
||||
value: "new-type-value"
|
||||
}
|
||||
}
|
||||
|
||||
await module.update([updateData])
|
||||
|
||||
product = await module.retrieve(updateData.id, {
|
||||
relations: ["type"]
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
type: expect.objectContaining({
|
||||
...updateData.type
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should replace relationships of a product", async () => {
|
||||
const newTagData = {
|
||||
id: "tag-2",
|
||||
value: "tag 2",
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
categories: [{
|
||||
id: productCategoryTwo.id
|
||||
}],
|
||||
collection_id: productCollectionTwo.id,
|
||||
type_id: productTypeTwo.id,
|
||||
tags: [newTagData],
|
||||
}
|
||||
|
||||
await module.update([updateData])
|
||||
|
||||
const product = await module.retrieve(updateData.id, {
|
||||
relations: ["categories", "collection", "tags", "type"]
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
categories: [
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id
|
||||
})
|
||||
],
|
||||
collection: expect.objectContaining({
|
||||
id: productCollectionTwo.id
|
||||
}),
|
||||
tags: [
|
||||
expect.objectContaining({
|
||||
id: newTagData.id,
|
||||
value: newTagData.value
|
||||
})
|
||||
],
|
||||
type: expect.objectContaining({
|
||||
id: productTypeTwo.id
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should remove relationships of a product", async () => {
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
categories: [],
|
||||
collection_id: null,
|
||||
type_id: null,
|
||||
tags: []
|
||||
}
|
||||
|
||||
await module.update([updateData])
|
||||
|
||||
const product = await module.retrieve(updateData.id, {
|
||||
relations: ["categories", "collection", "tags"]
|
||||
})
|
||||
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productTwo.id,
|
||||
categories: [],
|
||||
tags: [],
|
||||
collection: null,
|
||||
type: null
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when product ID does not exist", async () => {
|
||||
let error
|
||||
const updateData = {
|
||||
id: "does-not-exist",
|
||||
title: "test"
|
||||
}
|
||||
|
||||
try {
|
||||
await module.update([updateData])
|
||||
} catch (e) {
|
||||
error = e.message
|
||||
}
|
||||
|
||||
expect(error).toEqual(`Product with id "does-not-exist" not found`)
|
||||
})
|
||||
|
||||
it("should update, create and delete variants", async () => {
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
// Note: VariantThree is already assigned to productTwo, that should be deleted
|
||||
variants: [{
|
||||
id: variantTwo.id,
|
||||
title: "updated-variant"
|
||||
}, {
|
||||
title: "created-variant"
|
||||
}]
|
||||
}
|
||||
|
||||
await module.update([updateData])
|
||||
|
||||
const product = await module.retrieve(updateData.id, {
|
||||
relations: ["variants"]
|
||||
})
|
||||
|
||||
expect(product.variants).toHaveLength(2)
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: variantTwo.id,
|
||||
title: "updated-variant",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "created-variant",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when variant with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
const updateData = {
|
||||
id: productTwo.id,
|
||||
// Note: VariantThree is already assigned to productTwo, that should be deleted
|
||||
variants: [{
|
||||
id: "does-not-exist",
|
||||
title: "updated-variant"
|
||||
}, {
|
||||
title: "created-variant"
|
||||
}]
|
||||
}
|
||||
|
||||
try {
|
||||
await module.update([updateData])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
await module.retrieve(updateData.id, {
|
||||
relations: ["variants"]
|
||||
})
|
||||
|
||||
expect(error.message).toEqual(`ProductVariant with id "does-not-exist" not found`)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
variantsData,
|
||||
} from "../../../__fixtures__/product/data"
|
||||
|
||||
import { ProductDTO } from "@medusajs/types"
|
||||
import { ProductDTO, ProductTypes } from "@medusajs/types"
|
||||
import { ProductRepository } from "@repositories"
|
||||
import { ProductService } from "@services"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
@@ -27,6 +27,7 @@ describe("Product Service", () => {
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
let products!: Product[]
|
||||
let productOne: Product
|
||||
let variants!: ProductVariant[]
|
||||
let categories!: ProductCategory[]
|
||||
|
||||
@@ -47,6 +48,51 @@ describe("Product Service", () => {
|
||||
await TestDatabase.clearDatabase()
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([productOne])
|
||||
})
|
||||
|
||||
it("should throw an error when an id is not provided", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve(undefined as unknown as string)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('"productId" must be defined')
|
||||
})
|
||||
|
||||
it("should throw an error when product with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('Product with id: does-not-exist was not found')
|
||||
})
|
||||
|
||||
it("should return a product when product with an id exists", async () => {
|
||||
const result = await service.retrieve(productOne.id)
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
id: productOne.id
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
@@ -87,6 +133,94 @@ describe("Product Service", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
images = await createImages(testManager, ["image-1", "image-2"])
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([productOne])
|
||||
})
|
||||
|
||||
it("should update a product and its allowed relations", async () => {
|
||||
const updateData = [{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
images: images,
|
||||
thumbnail: images[0].url,
|
||||
}]
|
||||
|
||||
const products = await service.update(updateData)
|
||||
|
||||
expect(products.length).toEqual(1)
|
||||
|
||||
let result = await service.retrieve(productOne.id, {relations: ["images", "thumbnail"]})
|
||||
let serialized = JSON.parse(JSON.stringify(result))
|
||||
|
||||
expect(serialized).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
thumbnail: images[0].url,
|
||||
images: [
|
||||
expect.objectContaining({
|
||||
url: images[0].url,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
url: images[1].url,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when id is not present", async () => {
|
||||
let error
|
||||
const updateData = [{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
}, {
|
||||
id: undefined as unknown as string,
|
||||
title: "update test 2",
|
||||
}]
|
||||
|
||||
try {
|
||||
await service.update(updateData)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(`Product with id "undefined" not found`)
|
||||
|
||||
let result = await service.retrieve(productOne.id)
|
||||
|
||||
expect(result.title).not.toBe("update test 1")
|
||||
})
|
||||
|
||||
it("should throw an error when product with id does not exist", async () => {
|
||||
let error
|
||||
const updateData = [{
|
||||
id: "does-not-exist",
|
||||
title: "update test 1",
|
||||
}]
|
||||
|
||||
try {
|
||||
await service.update(updateData)
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(`Product with id "does-not-exist" not found`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
describe("soft deleted", function () {
|
||||
let deletedProduct
|
||||
|
||||
Reference in New Issue
Block a user