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:
Riqwan Thamir
2023-07-26 15:18:35 +02:00
committed by GitHub
co-authored by Oliver Windall Juhl adrien2p Carlos R. L. Rodrigues
parent 585ebf2454
commit caea44ebfd
18 changed files with 1299 additions and 41 deletions
@@ -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