feat(product): Create (+ workflow), delete, restore (#4459)

* 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

* update repositories manager usage and serialisation from the write public API

* move serializisation to the DAL

* rename template args

* 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>

* feat(product): Apply transaction decorators to the services (#4512)

---------

Co-authored-by: Riqwan Thamir <rmthamir@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:
Adrien de Peretti
2023-07-16 20:19:23 +02:00
committed by GitHub
co-authored by Oliver Windall Juhl Riqwan Thamir Carlos R. L. Rodrigues
parent 5b91a3503a
commit befc2f1c80
98 changed files with 5444 additions and 688 deletions
@@ -0,0 +1,159 @@
import { EntityManager } from "typeorm"
import {
IInventoryService,
MedusaContainer,
ProductTypes,
} from "@medusajs/types"
import { ulid } from "ulid"
import { MedusaError } from "@medusajs/utils"
import {
DistributedTransaction,
TransactionHandlerType,
TransactionOrchestrator,
TransactionPayload,
TransactionState,
TransactionStepsDefinition,
} from "../../../utils/transaction"
import { CreateProductVariantInput } from "../../../types/product-variant"
import {
attachInventoryItems,
createInventoryItems,
createProducts,
removeInventoryItems,
removeProducts,
} from "../../functions"
enum Actions {
createProduct = "createProduct",
createPrices = "createPrices",
attachToSalesChannel = "attachToSalesChannel",
createInventoryItems = "createInventoryItems",
attachInventoryItems = "attachInventoryItems",
}
const workflowSteps: TransactionStepsDefinition = {
next: {
action: Actions.createProduct,
saveResponse: true,
next: {
action: Actions.attachToSalesChannel,
saveResponse: true,
next: {
action: Actions.createPrices,
saveResponse: true,
next: {
action: Actions.createInventoryItems,
saveResponse: true,
next: {
action: Actions.attachInventoryItems,
noCompensation: true,
},
},
},
},
},
}
const createProductOrchestrator = new TransactionOrchestrator(
"create-product",
workflowSteps
)
type InjectedDependencies = {
manager: EntityManager
container: MedusaContainer
inventoryService?: IInventoryService
}
export async function createProductWorkflow(
dependencies: InjectedDependencies,
productId: string,
input: CreateProductVariantInput[]
): Promise<DistributedTransaction> {
const { manager, container } = dependencies
async function transactionHandler(
actionId: string,
type: TransactionHandlerType,
payload: TransactionPayload
) {
const command = {
[Actions.createProduct]: {
[TransactionHandlerType.INVOKE]: async (
data: ProductTypes.CreateProductDTO[]
) => {
return await createProducts({
container,
data,
})
},
[TransactionHandlerType.COMPENSATE]: async (
data: any[],
{ invoke }
) => {
const createdProducts = invoke[Actions.createProduct]
return await removeProducts({ container, data: createdProducts })
},
},
[Actions.createInventoryItems]: {
[TransactionHandlerType.INVOKE]: async (
data: CreateProductVariantInput[],
{ invoke }
) => {
const { [Actions.createProduct]: products } = invoke
return await createInventoryItems({
container,
manager,
data: products,
})
},
[TransactionHandlerType.COMPENSATE]: async (_, { invoke }) => {
const variantInventoryItemsData = invoke[Actions.createInventoryItems]
await removeInventoryItems({
container,
manager,
data: variantInventoryItemsData,
})
},
},
[Actions.attachInventoryItems]: {
[TransactionHandlerType.INVOKE]: async (
data: CreateProductVariantInput[],
{ invoke }
) => {
const { [Actions.createInventoryItems]: inventoryItemsResult } =
invoke
return await attachInventoryItems({
container,
manager,
data: inventoryItemsResult,
})
},
},
}
return command[actionId][type](payload.data, payload.context)
}
const orchestrator = createProductOrchestrator
const transaction = await orchestrator.beginTransaction(
ulid(),
transactionHandler,
input
)
await orchestrator.resume(transaction)
if (transaction.getState() !== TransactionState.DONE) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
transaction
.getErrors()
.map((err) => err.error?.message)
.join("\n")
)
}
return transaction
}
@@ -0,0 +1,34 @@
import {
InventoryItemDTO,
MedusaContainer,
ProductTypes,
} from "@medusajs/types"
import { EntityManager } from "typeorm"
export async function attachInventoryItems({
container,
manager,
data,
}: {
container: MedusaContainer
manager: EntityManager
data: {
variant: ProductTypes.ProductVariantDTO
inventoryItem: InventoryItemDTO
}[]
}) {
const productVariantInventoryService = container
.resolve("productVariantInventoryService")
.withTransaction(manager)
return await Promise.all(
data
.filter((d) => d)
.map(async ({ variant, inventoryItem }) => {
return await productVariantInventoryService.attachInventoryItem(
variant.id,
inventoryItem.id
)
})
)
}
@@ -0,0 +1,55 @@
import {
IInventoryService,
MedusaContainer,
ProductTypes,
} from "@medusajs/types"
import { EntityManager } from "typeorm"
export async function createInventoryItems({
container,
manager,
data,
}: {
container: MedusaContainer
manager: EntityManager
data: ProductTypes.ProductDTO[]
}) {
const inventoryService: IInventoryService =
container.resolve("inventoryService")
const context = { transactionManager: manager }
const variants = data.reduce(
(
acc: ProductTypes.ProductVariantDTO[],
product: ProductTypes.ProductDTO
) => {
return acc.concat(product.variants)
},
[]
)
return await Promise.all(
variants.map(async (variant) => {
if (!variant.manage_inventory) {
return
}
const inventoryItem = await inventoryService!.createInventoryItem(
{
sku: variant.sku!,
origin_country: variant.origin_country!,
hs_code: variant.hs_code!,
mid_code: variant.mid_code!,
material: variant.material!,
weight: variant.weight!,
length: variant.length!,
height: variant.height!,
width: variant.width!,
},
context
)
return { variant, inventoryItem }
})
)
}
@@ -0,0 +1,12 @@
import { MedusaContainer, ProductTypes } from "@medusajs/types"
export async function removeProducts({
container,
data,
}: {
container: MedusaContainer
data: ProductTypes.ProductDTO[]
}): Promise<ProductTypes.ProductDTO[]> {
const productModuleService = container.resolve("productModuleService")
return await productModuleService.softDelete(data.map((p) => p.id))
}
@@ -0,0 +1,5 @@
export * from "./create-prducts"
export * from "./remove-products"
export * from "./create-inventory-items"
export * from "./remove-inventory-items"
export * from "./attach-inventory-items"
@@ -0,0 +1,26 @@
import { InventoryItemDTO, MedusaContainer } from "@medusajs/types"
import { EntityManager } from "typeorm"
export async function removeInventoryItems({
container,
manager,
data,
}: {
container: MedusaContainer
manager: EntityManager
data: {
inventoryItem: InventoryItemDTO
}[]
}) {
const inventoryService = container.resolve("inventoryService")
const context = { transactionManager: manager }
return await Promise.all(
data.map(async ({ inventoryItem }) => {
return await inventoryService!.deleteInventoryItem(
inventoryItem.id,
context
)
})
)
}
@@ -0,0 +1,12 @@
import { MedusaContainer, ProductTypes } from "@medusajs/types"
export async function createProducts({
container,
data,
}: {
container: MedusaContainer
data: ProductTypes.CreateProductDTO[]
}) {
const productModuleService = container.resolve("productModuleService")
return await productModuleService.create(data)
}