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:
18 files changed
+1299
-41
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/product": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(product, types): added product module service update
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Product,
|
Product,
|
||||||
ProductCategory,
|
ProductCategory,
|
||||||
ProductCollection,
|
ProductCollection,
|
||||||
|
ProductType,
|
||||||
ProductVariant,
|
ProductVariant,
|
||||||
} from "@models"
|
} from "@models"
|
||||||
import ProductOption from "../../../src/models/product-option"
|
import ProductOption from "../../../src/models/product-option"
|
||||||
@@ -59,6 +60,22 @@ export async function createCollections(
|
|||||||
return collections
|
return collections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createTypes(
|
||||||
|
manager: SqlEntityManager,
|
||||||
|
typesData: {
|
||||||
|
id?: string
|
||||||
|
value: string
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
const types: any[] = typesData.map((typesData) => {
|
||||||
|
return manager.create(ProductType, typesData)
|
||||||
|
})
|
||||||
|
|
||||||
|
await manager.persistAndFlush(types)
|
||||||
|
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
export async function createOptions(
|
export async function createOptions(
|
||||||
manager: SqlEntityManager,
|
manager: SqlEntityManager,
|
||||||
optionsData: {
|
optionsData: {
|
||||||
|
|||||||
+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,
|
variantsData,
|
||||||
} from "../../../__fixtures__/product/data"
|
} from "../../../__fixtures__/product/data"
|
||||||
|
|
||||||
import { ProductDTO } from "@medusajs/types"
|
import { ProductDTO, ProductTypes } from "@medusajs/types"
|
||||||
import { ProductRepository } from "@repositories"
|
import { ProductRepository } from "@repositories"
|
||||||
import { ProductService } from "@services"
|
import { ProductService } from "@services"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
@@ -27,6 +27,7 @@ describe("Product Service", () => {
|
|||||||
let testManager: SqlEntityManager
|
let testManager: SqlEntityManager
|
||||||
let repositoryManager: SqlEntityManager
|
let repositoryManager: SqlEntityManager
|
||||||
let products!: Product[]
|
let products!: Product[]
|
||||||
|
let productOne: Product
|
||||||
let variants!: ProductVariant[]
|
let variants!: ProductVariant[]
|
||||||
let categories!: ProductCategory[]
|
let categories!: ProductCategory[]
|
||||||
|
|
||||||
@@ -47,6 +48,51 @@ describe("Product Service", () => {
|
|||||||
await TestDatabase.clearDatabase()
|
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 () {
|
describe("create", function () {
|
||||||
let images: Image[] = []
|
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("list", () => {
|
||||||
describe("soft deleted", function () {
|
describe("soft deleted", function () {
|
||||||
let deletedProduct
|
let deletedProduct
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class Product {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
fieldName: "collection_id",
|
fieldName: "collection_id",
|
||||||
})
|
})
|
||||||
collection!: ProductCollection
|
collection!: ProductCollection | null
|
||||||
|
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
type_id!: string
|
type_id!: string
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import {
|
|||||||
LoadStrategy,
|
LoadStrategy,
|
||||||
RequiredEntityData,
|
RequiredEntityData,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
import { Product, ProductVariant } from "@models"
|
import { ProductVariant } from "@models"
|
||||||
import { Context, DAL } from "@medusajs/types"
|
import { Context, DAL, WithRequiredProperty } from "@medusajs/types"
|
||||||
import { AbstractBaseRepository } from "./base"
|
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
import {
|
||||||
|
MedusaError,
|
||||||
|
isDefined,
|
||||||
|
InjectTransactionManager,
|
||||||
|
MedusaContext,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
|
import { ProductVariantServiceTypes } from "../types/services"
|
||||||
|
import { AbstractBaseRepository } from "./base"
|
||||||
import { doNotForceTransaction } from "../utils"
|
import { doNotForceTransaction } from "../utils"
|
||||||
|
|
||||||
export class ProductVariantRepository extends AbstractBaseRepository<ProductVariant> {
|
export class ProductVariantRepository extends AbstractBaseRepository<ProductVariant> {
|
||||||
@@ -69,7 +76,7 @@ export class ProductVariantRepository extends AbstractBaseRepository<ProductVari
|
|||||||
{ transactionManager: manager }: Context = {}
|
{ transactionManager: manager }: Context = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await (manager as SqlEntityManager).nativeDelete(
|
await (manager as SqlEntityManager).nativeDelete(
|
||||||
Product,
|
ProductVariant,
|
||||||
{ id: { $in: ids } },
|
{ id: { $in: ids } },
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
@@ -89,4 +96,37 @@ export class ProductVariantRepository extends AbstractBaseRepository<ProductVari
|
|||||||
|
|
||||||
return variants
|
return variants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
data: WithRequiredProperty<ProductVariantServiceTypes.UpdateProductVariantDTO, "id">[],
|
||||||
|
context: Context = {}
|
||||||
|
): Promise<ProductVariant[]> {
|
||||||
|
const manager = (context.transactionManager ??
|
||||||
|
this.manager_) as SqlEntityManager
|
||||||
|
|
||||||
|
const productVariantsToUpdate = await manager.find(ProductVariant, {
|
||||||
|
id: data.map((updateData) => updateData.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const productVariantsToUpdateMap = new Map<string, ProductVariant>(
|
||||||
|
productVariantsToUpdate.map((variant) => [variant.id, variant])
|
||||||
|
)
|
||||||
|
|
||||||
|
const variants = data.map((variantData) => {
|
||||||
|
const productVariant = productVariantsToUpdateMap.get(variantData.id)
|
||||||
|
|
||||||
|
if (!productVariant) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`ProductVariant with id "${variantData.id}" not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager.assign(productVariant, variantData)
|
||||||
|
})
|
||||||
|
|
||||||
|
await manager.persist(variants)
|
||||||
|
|
||||||
|
return variants
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,29 @@
|
|||||||
import { Product } from "@models"
|
import {
|
||||||
|
Product,
|
||||||
|
ProductCategory,
|
||||||
|
ProductCollection,
|
||||||
|
ProductType,
|
||||||
|
ProductTag,
|
||||||
|
} from "@models"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FilterQuery as MikroFilterQuery,
|
FilterQuery as MikroFilterQuery,
|
||||||
FindOptions as MikroOptions,
|
FindOptions as MikroOptions,
|
||||||
LoadStrategy,
|
LoadStrategy,
|
||||||
|
wrap
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Context,
|
Context,
|
||||||
DAL,
|
DAL,
|
||||||
ProductTypes,
|
ProductTypes,
|
||||||
WithRequiredProperty,
|
WithRequiredProperty,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { AbstractBaseRepository } from "./base"
|
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
import { MedusaError, isDefined, InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||||
|
|
||||||
|
import { AbstractBaseRepository } from "./base"
|
||||||
|
import { ProductServiceTypes } from "../types/services"
|
||||||
|
|
||||||
export class ProductRepository extends AbstractBaseRepository<Product> {
|
export class ProductRepository extends AbstractBaseRepository<Product> {
|
||||||
protected readonly manager_: SqlEntityManager
|
protected readonly manager_: SqlEntityManager
|
||||||
@@ -27,6 +38,7 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
|
|||||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||||
context: Context = {}
|
context: Context = {}
|
||||||
): Promise<Product[]> {
|
): Promise<Product[]> {
|
||||||
|
// TODO: use the getter method (getActiveManager)
|
||||||
const manager = (context.transactionManager ??
|
const manager = (context.transactionManager ??
|
||||||
this.manager_) as SqlEntityManager
|
this.manager_) as SqlEntityManager
|
||||||
|
|
||||||
@@ -78,6 +90,7 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
|
|||||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||||
context: Context = {}
|
context: Context = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// TODO: use the getter method (getActiveManager)
|
||||||
const manager = (context.transactionManager ??
|
const manager = (context.transactionManager ??
|
||||||
this.manager_) as SqlEntityManager
|
this.manager_) as SqlEntityManager
|
||||||
|
|
||||||
@@ -134,4 +147,165 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
|
|||||||
|
|
||||||
return products
|
return products
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager()
|
||||||
|
async update(
|
||||||
|
data: WithRequiredProperty<ProductServiceTypes.UpdateProductDTO, "id">[],
|
||||||
|
@MedusaContext() context: Context = {}
|
||||||
|
): Promise<Product[]> {
|
||||||
|
let categoryIds: string[] = []
|
||||||
|
let tagIds: string[] = []
|
||||||
|
let collectionIds: string[] = []
|
||||||
|
let typeIds: string[] = []
|
||||||
|
// TODO: use the getter method (getActiveManager)
|
||||||
|
const manager = (context.transactionManager ??
|
||||||
|
this.manager_) as SqlEntityManager
|
||||||
|
|
||||||
|
data.forEach((productData) => {
|
||||||
|
categoryIds = categoryIds.concat(
|
||||||
|
productData?.categories?.map(c => c.id) || []
|
||||||
|
)
|
||||||
|
|
||||||
|
tagIds = tagIds.concat(
|
||||||
|
productData?.tags?.map(c => c.id) || []
|
||||||
|
)
|
||||||
|
|
||||||
|
if (productData.collection_id) {
|
||||||
|
collectionIds.push(productData.collection_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productData.type_id) {
|
||||||
|
typeIds.push(productData.type_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const productsToUpdate = await manager.find(Product, {
|
||||||
|
id: data.map((updateData) => updateData.id)
|
||||||
|
}, {
|
||||||
|
populate: ["tags", "categories"]
|
||||||
|
})
|
||||||
|
|
||||||
|
const collectionsToAssign = collectionIds.length ? await manager.find(ProductCollection, {
|
||||||
|
id: collectionIds
|
||||||
|
}) : []
|
||||||
|
|
||||||
|
const typesToAssign = typeIds.length ? await manager.find(ProductType, {
|
||||||
|
id: typeIds
|
||||||
|
}) : []
|
||||||
|
|
||||||
|
const categoriesToAssign = categoryIds.length ? await manager.find(ProductCategory, {
|
||||||
|
id: categoryIds
|
||||||
|
}) : []
|
||||||
|
|
||||||
|
const tagsToAssign = tagIds.length ? await manager.find(ProductTag, {
|
||||||
|
id: tagIds
|
||||||
|
}) : []
|
||||||
|
|
||||||
|
const categoriesToAssignMap = new Map<string, ProductCategory>(
|
||||||
|
categoriesToAssign.map((category) => [category.id, category])
|
||||||
|
)
|
||||||
|
|
||||||
|
const tagsToAssignMap = new Map<string, ProductTag>(
|
||||||
|
tagsToAssign.map((tag) => [tag.id, tag])
|
||||||
|
)
|
||||||
|
|
||||||
|
const collectionsToAssignMap = new Map<string, ProductCollection>(
|
||||||
|
collectionsToAssign.map((collection) => [collection.id, collection])
|
||||||
|
)
|
||||||
|
|
||||||
|
const typesToAssignMap = new Map<string, ProductType>(
|
||||||
|
typesToAssign.map((type) => [type.id, type])
|
||||||
|
)
|
||||||
|
|
||||||
|
const productsToUpdateMap = new Map<string, Product>(
|
||||||
|
productsToUpdate.map((product) => [product.id, product])
|
||||||
|
)
|
||||||
|
|
||||||
|
const products = await Promise.all(
|
||||||
|
data.map(async (updateData) => {
|
||||||
|
const product = productsToUpdateMap.get(updateData.id)
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Product with id "${updateData.id}" not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
categories: categoriesData,
|
||||||
|
tags: tagsData,
|
||||||
|
collection_id: collectionId,
|
||||||
|
type_id: typeId,
|
||||||
|
} = updateData
|
||||||
|
|
||||||
|
delete updateData?.categories
|
||||||
|
delete updateData?.tags
|
||||||
|
delete updateData?.collection_id
|
||||||
|
delete updateData?.type_id
|
||||||
|
|
||||||
|
if (isDefined(categoriesData)) {
|
||||||
|
await product.categories.init()
|
||||||
|
|
||||||
|
for (const categoryData of categoriesData) {
|
||||||
|
const productCategory = categoriesToAssignMap.get(categoryData.id)
|
||||||
|
|
||||||
|
if (productCategory) {
|
||||||
|
await product.categories.add(productCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryIdsToAssignSet = new Set(categoriesData.map(cd => cd.id))
|
||||||
|
const categoriesToDelete = product.categories.getItems().filter(
|
||||||
|
(existingCategory) => !categoryIdsToAssignSet.has(existingCategory.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await product.categories.remove(categoriesToDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(tagsData)) {
|
||||||
|
await product.tags.init()
|
||||||
|
|
||||||
|
for (const tagData of tagsData) {
|
||||||
|
let productTag = tagsToAssignMap.get(tagData.id)
|
||||||
|
|
||||||
|
if (tagData instanceof ProductTag) {
|
||||||
|
productTag = tagData
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productTag) {
|
||||||
|
await product.tags.add(productTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagIdsToAssignSet = new Set(tagsData.map(cd => cd.id))
|
||||||
|
const tagsToDelete = product.tags.getItems().filter(
|
||||||
|
(existingTag) => !tagIdsToAssignSet.has(existingTag.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await product.tags.remove(tagsToDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(collectionId)) {
|
||||||
|
const collection = collectionsToAssignMap.get(collectionId)
|
||||||
|
|
||||||
|
product.collection = collection || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(typeId)) {
|
||||||
|
const type = typesToAssignMap.get(typeId)
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
product.type = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager.assign(product, updateData)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await manager.persist(products)
|
||||||
|
|
||||||
|
return products
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,13 +26,17 @@ import {
|
|||||||
JoinerServiceConfig,
|
JoinerServiceConfig,
|
||||||
ProductTypes,
|
ProductTypes,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
import { serialize } from "@mikro-orm/core"
|
||||||
|
|
||||||
import ProductImageService from "./product-image"
|
import ProductImageService from "./product-image"
|
||||||
|
import { ProductServiceTypes, ProductVariantServiceTypes } from "../types/services"
|
||||||
import {
|
import {
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
isDefined,
|
isDefined,
|
||||||
isString,
|
isString,
|
||||||
kebabCase,
|
kebabCase,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { shouldForceTransaction } from "../utils"
|
import { shouldForceTransaction } from "../utils"
|
||||||
import { joinerConfig } from "./../joiner-config"
|
import { joinerConfig } from "./../joiner-config"
|
||||||
@@ -118,10 +122,12 @@ export default class ProductModuleService<
|
|||||||
|
|
||||||
async retrieve(
|
async retrieve(
|
||||||
productId: string,
|
productId: string,
|
||||||
|
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<ProductTypes.ProductDTO> {
|
): Promise<ProductTypes.ProductDTO> {
|
||||||
const product = await this.productService_.retrieve(
|
const product = await this.productService_.retrieve(
|
||||||
productId,
|
productId,
|
||||||
|
config,
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -282,7 +288,7 @@ export default class ProductModuleService<
|
|||||||
return JSON.parse(JSON.stringify(categories))
|
return JSON.parse(JSON.stringify(categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context) {
|
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context): Promise<ProductTypes.ProductDTO[]> {
|
||||||
const products = await this.create_(data, sharedContext)
|
const products = await this.create_(data, sharedContext)
|
||||||
|
|
||||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
||||||
@@ -290,6 +296,19 @@ export default class ProductModuleService<
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
data: ProductTypes.UpdateProductDTO[],
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<ProductTypes.ProductDTO[]> {
|
||||||
|
const products = await this.update_(data, sharedContext)
|
||||||
|
|
||||||
|
return this.baseRepository_.serialize<
|
||||||
|
ProductTypes.ProductDTO[]
|
||||||
|
>(products, {
|
||||||
|
populate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||||
protected async create_(
|
protected async create_(
|
||||||
data: ProductTypes.CreateProductDTO[],
|
data: ProductTypes.CreateProductDTO[],
|
||||||
@@ -329,30 +348,9 @@ export default class ProductModuleService<
|
|||||||
productData.discountable = false
|
productData.discountable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (productData.images?.length) {
|
await this.upsertAndAssignImagesToProductData(productData, sharedContext)
|
||||||
productData.images = await this.productImageService_.upsert(
|
await this.upsertAndAssignProductTagsToProductData(productData, sharedContext)
|
||||||
productData.images.map((image) =>
|
await this.upsertAndAssignProductTypeToProductData(productData, sharedContext)
|
||||||
isString(image) ? image : image.url
|
|
||||||
),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (productData.tags?.length) {
|
|
||||||
productData.tags = await this.productTagService_.upsert(
|
|
||||||
productData.tags,
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDefined(productData.type)) {
|
|
||||||
productData.type = (
|
|
||||||
await this.productTypeService_.upsert(
|
|
||||||
[productData.type as ProductTypes.CreateProductTypeDTO],
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
)?.[0]!
|
|
||||||
}
|
|
||||||
|
|
||||||
return productData as CreateProductOnlyDTO
|
return productData as CreateProductOnlyDTO
|
||||||
})
|
})
|
||||||
@@ -408,6 +406,223 @@ export default class ProductModuleService<
|
|||||||
return products
|
return products
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||||
|
protected async update_(
|
||||||
|
data: ProductTypes.UpdateProductDTO[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<TProduct[]> {
|
||||||
|
const productIds = data.map(pd => pd.id)
|
||||||
|
const existingProductVariants = await this.productVariantService_.list(
|
||||||
|
{ product_id: productIds },
|
||||||
|
{},
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
const existingProductVariantsMap = new Map<
|
||||||
|
string,
|
||||||
|
ProductVariant[]
|
||||||
|
>(
|
||||||
|
data.map((productData) => {
|
||||||
|
const productVariantsForProduct = existingProductVariants
|
||||||
|
.filter((variant) => variant.product_id === productData.id)
|
||||||
|
|
||||||
|
return [
|
||||||
|
productData.id,
|
||||||
|
productVariantsForProduct,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const productVariantsMap = new Map<
|
||||||
|
string,
|
||||||
|
(ProductTypes.CreateProductVariantDTO | ProductTypes.UpdateProductVariantDTO)[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
const productOptionsMap = new Map<
|
||||||
|
string,
|
||||||
|
ProductTypes.CreateProductOptionDTO[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
const productsData = await Promise.all(
|
||||||
|
data.map(async (product) => {
|
||||||
|
const { variants, options, ...productData } = product
|
||||||
|
|
||||||
|
if (!isDefined(productData.id)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Cannot update product without id`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
productVariantsMap.set(productData.id, variants ?? [])
|
||||||
|
productOptionsMap.set(productData.id, options ?? [])
|
||||||
|
|
||||||
|
if (productData.is_giftcard) {
|
||||||
|
productData.discountable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.upsertAndAssignImagesToProductData(productData, sharedContext)
|
||||||
|
await this.upsertAndAssignProductTagsToProductData(productData, sharedContext)
|
||||||
|
await this.upsertAndAssignProductTypeToProductData(productData, sharedContext)
|
||||||
|
|
||||||
|
return productData as ProductServiceTypes.UpdateProductDTO
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const products = await this.productService_.update(
|
||||||
|
productsData,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
const productByIdMap = new Map<string, TProduct>(
|
||||||
|
products.map((product) => [product.id, product])
|
||||||
|
)
|
||||||
|
|
||||||
|
const productOptionsData = [...productOptionsMap]
|
||||||
|
.map(([id, options]) => options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
product: productByIdMap.get(id)!,
|
||||||
|
})))
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
const productOptions = await this.productOptionService_.create(
|
||||||
|
productOptionsData,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
const productVariantIdsToDelete: string[] = []
|
||||||
|
const productVariantsToCreateMap = new Map<
|
||||||
|
string,
|
||||||
|
ProductTypes.CreateProductVariantDTO[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
const productVariantsToUpdateMap = new Map<
|
||||||
|
string,
|
||||||
|
ProductTypes.UpdateProductVariantDTO[]
|
||||||
|
>()
|
||||||
|
|
||||||
|
for (const [productId, variants] of productVariantsMap) {
|
||||||
|
const variantsToCreate: ProductTypes.CreateProductVariantDTO[] = []
|
||||||
|
const variantsToUpdate: ProductTypes.UpdateProductVariantDTO[] = []
|
||||||
|
const existingVariants = existingProductVariantsMap.get(productId)
|
||||||
|
|
||||||
|
variants.forEach((variant) => {
|
||||||
|
const isVariantIdDefined = ("id" in variant) && isDefined(variant.id)
|
||||||
|
|
||||||
|
if (isVariantIdDefined) {
|
||||||
|
variantsToUpdate.push(variant as ProductTypes.UpdateProductVariantDTO)
|
||||||
|
} else {
|
||||||
|
variantsToCreate.push(variant as ProductTypes.CreateProductVariantDTO)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantOptions = variant.options?.map((option, index) => {
|
||||||
|
const productOption = productOptions[index]
|
||||||
|
return {
|
||||||
|
option: productOption,
|
||||||
|
value: option.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (variantOptions) {
|
||||||
|
variant.options = variantOptions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
productVariantsToCreateMap.set(productId, variantsToCreate)
|
||||||
|
productVariantsToUpdateMap.set(productId, variantsToUpdate)
|
||||||
|
|
||||||
|
const variantsToUpdateIds = variantsToUpdate.map(v => v?.id) as string[]
|
||||||
|
const existingVariantIds = existingVariants?.map(v => v.id) || []
|
||||||
|
const variantsToUpdateSet = new Set(variantsToUpdateIds)
|
||||||
|
|
||||||
|
productVariantIdsToDelete.push(
|
||||||
|
...new Set(
|
||||||
|
existingVariantIds.filter(x => !variantsToUpdateSet.has(x))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
|
productVariantsToCreateMap.forEach((variants, productId) => {
|
||||||
|
promises.push(
|
||||||
|
this.productVariantService_.create(
|
||||||
|
productByIdMap.get(productId)!,
|
||||||
|
variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[],
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
productVariantsToUpdateMap.forEach((variants, productId) => {
|
||||||
|
promises.push(
|
||||||
|
this.productVariantService_.update(
|
||||||
|
productByIdMap.get(productId)!,
|
||||||
|
variants as unknown as ProductVariantServiceTypes.UpdateProductVariantDTO[],
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (productVariantIdsToDelete.length) {
|
||||||
|
promises.push(
|
||||||
|
this.productVariantService_.delete(productVariantIdsToDelete, sharedContext)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
return products
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async upsertAndAssignImagesToProductData(
|
||||||
|
productData: ProductTypes.CreateProductDTO | ProductTypes.UpdateProductDTO,
|
||||||
|
sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
if (!productData.thumbnail && productData.images?.length) {
|
||||||
|
productData.thumbnail = isString(productData.images[0])
|
||||||
|
? (productData.images[0] as string)
|
||||||
|
: (productData.images[0] as { url: string }).url
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productData.images?.length) {
|
||||||
|
productData.images = await this.productImageService_.upsert(
|
||||||
|
productData.images.map((image) =>
|
||||||
|
isString(image) ? image : image.url
|
||||||
|
),
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async upsertAndAssignProductTagsToProductData(
|
||||||
|
productData: ProductTypes.CreateProductDTO | ProductTypes.UpdateProductDTO,
|
||||||
|
sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
if (productData.tags?.length) {
|
||||||
|
productData.tags = await this.productTagService_.upsert(
|
||||||
|
productData.tags,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async upsertAndAssignProductTypeToProductData(
|
||||||
|
productData: ProductTypes.CreateProductDTO | ProductTypes.UpdateProductDTO,
|
||||||
|
sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
if (isDefined(productData.type)) {
|
||||||
|
const productType = (
|
||||||
|
await this.productTypeService_.upsert(
|
||||||
|
[productData.type as ProductTypes.CreateProductTypeDTO],
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
productData.type = productType?.[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||||
async delete(
|
async delete(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Product, ProductVariant } from "@models"
|
import { Product, ProductVariant } from "@models"
|
||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||||
|
import { ProductVariantRepository } from "@repositories"
|
||||||
import {
|
import {
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
isString,
|
isString,
|
||||||
@@ -8,9 +9,9 @@ import {
|
|||||||
retrieveEntity,
|
retrieveEntity,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
|
import { ProductVariantServiceTypes } from "../types/services"
|
||||||
import ProductService from "./product"
|
import ProductService from "./product"
|
||||||
import { doNotForceTransaction } from "../utils"
|
import { doNotForceTransaction } from "../utils"
|
||||||
import { ProductVariantRepository } from "@repositories"
|
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
productVariantRepository: DAL.RepositoryService
|
productVariantRepository: DAL.RepositoryService
|
||||||
@@ -91,7 +92,8 @@ export default class ProductVariantService<
|
|||||||
|
|
||||||
if (isString(productOrId)) {
|
if (isString(productOrId)) {
|
||||||
product = await this.productService_.retrieve(
|
product = await this.productService_.retrieve(
|
||||||
productOrId as string,
|
productOrId,
|
||||||
|
{},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -112,4 +114,38 @@ export default class ProductVariantService<
|
|||||||
transactionManager: sharedContext.transactionManager,
|
transactionManager: sharedContext.transactionManager,
|
||||||
})) as TEntity[]
|
})) as TEntity[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||||
|
async update(
|
||||||
|
productOrId: TProduct | string,
|
||||||
|
data: ProductVariantServiceTypes.UpdateProductVariantDTO[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<TEntity[]> {
|
||||||
|
let product = productOrId as unknown as Product
|
||||||
|
|
||||||
|
if (isString(productOrId)) {
|
||||||
|
product = await this.productService_.retrieve(
|
||||||
|
productOrId,
|
||||||
|
{},
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantsData = [...data]
|
||||||
|
variantsData.forEach((variant) => Object.assign(variant, { product }))
|
||||||
|
|
||||||
|
return await (this.productVariantRepository_ as ProductVariantRepository).update(variantsData, {
|
||||||
|
transactionManager: sharedContext.transactionManager,
|
||||||
|
}) as TEntity[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||||
|
async delete(
|
||||||
|
ids: string[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<void> {
|
||||||
|
return await this.productVariantRepository_.delete(ids, {
|
||||||
|
transactionManager: sharedContext.transactionManager,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,11 @@ import {
|
|||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
ModulesSdkUtils,
|
ModulesSdkUtils,
|
||||||
|
isDefined,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { ProductRepository } from "@repositories"
|
import { ProductRepository } from "@repositories"
|
||||||
|
|
||||||
|
import { ProductServiceTypes } from "../types/services"
|
||||||
import { doNotForceTransaction } from "../utils"
|
import { doNotForceTransaction } from "../utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
@@ -27,10 +30,22 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
this.productRepository_ = productRepository
|
this.productRepository_ = productRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieve(productId: string, sharedContext?: Context): Promise<TEntity> {
|
async retrieve(
|
||||||
|
productId: string,
|
||||||
|
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<TEntity> {
|
||||||
|
if (!isDefined(productId)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`"productId" must be defined`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
||||||
id: productId,
|
id: productId,
|
||||||
})
|
}, config)
|
||||||
|
|
||||||
const product = await this.productRepository_.find(
|
const product = await this.productRepository_.find(
|
||||||
queryOptions,
|
queryOptions,
|
||||||
sharedContext
|
sharedContext
|
||||||
@@ -116,6 +131,22 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
)) as TEntity[]
|
)) as TEntity[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||||
|
async update(
|
||||||
|
data: ProductServiceTypes.UpdateProductDTO[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<TEntity[]> {
|
||||||
|
return await (this.productRepository_ as ProductRepository).update(
|
||||||
|
data as WithRequiredProperty<
|
||||||
|
ProductServiceTypes.UpdateProductDTO,
|
||||||
|
"id"
|
||||||
|
>[],
|
||||||
|
{
|
||||||
|
transactionManager: sharedContext.transactionManager,
|
||||||
|
}
|
||||||
|
) as TEntity[]
|
||||||
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||||
async delete(
|
async delete(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ import { IEventBusService } from "@medusajs/types"
|
|||||||
export type InitializeModuleInjectableDependencies = {
|
export type InitializeModuleInjectableDependencies = {
|
||||||
eventBusService?: IEventBusService
|
eventBusService?: IEventBusService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from "./services"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * as ProductServiceTypes from "./product"
|
||||||
|
export * as ProductVariantServiceTypes from "./product-variant"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { CreateProductVariantOptionDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
export interface UpdateProductVariantDTO {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
sku?: string
|
||||||
|
barcode?: string
|
||||||
|
ean?: string
|
||||||
|
upc?: string
|
||||||
|
allow_backorder?: boolean
|
||||||
|
inventory_quantity?: number
|
||||||
|
manage_inventory?: boolean
|
||||||
|
hs_code?: string
|
||||||
|
origin_country?: string
|
||||||
|
mid_code?: string
|
||||||
|
material?: string
|
||||||
|
weight?: number
|
||||||
|
length?: number
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
options?: CreateProductVariantOptionDTO[]
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ProductStatus, ProductCategoryDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
export interface UpdateProductDTO {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
description?: string
|
||||||
|
is_giftcard?: boolean
|
||||||
|
discountable?: boolean
|
||||||
|
images?: { id?: string; url: string }[]
|
||||||
|
thumbnail?: string
|
||||||
|
handle?: string
|
||||||
|
status?: ProductStatus
|
||||||
|
collection_id?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
length?: number
|
||||||
|
weight?: number
|
||||||
|
origin_country?: string
|
||||||
|
hs_code?: string
|
||||||
|
material?: string
|
||||||
|
mid_code?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
tags?: { id: string }[]
|
||||||
|
categories?: { id: string }[]
|
||||||
|
type_id?: string
|
||||||
|
}
|
||||||
@@ -34,6 +34,9 @@ export interface RepositoryService<T = any> {
|
|||||||
|
|
||||||
create(data: unknown[], context?: Context): Promise<T[]>
|
create(data: unknown[], context?: Context): Promise<T[]>
|
||||||
|
|
||||||
|
// TODO: remove optionality when all the other repositories have an update
|
||||||
|
update?(data: unknown[], context?: Context): Promise<T[]>
|
||||||
|
|
||||||
delete(ids: string[], context?: Context): Promise<void>
|
delete(ids: string[], context?: Context): Promise<void>
|
||||||
|
|
||||||
softDelete(ids: string[], context?: Context): Promise<T[]>
|
softDelete(ids: string[], context?: Context): Promise<T[]>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface IInventoryService {
|
|||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
): Promise<ReservationItemDTO>
|
): Promise<ReservationItemDTO>
|
||||||
|
|
||||||
|
// TODO make it bulk
|
||||||
createReservationItems(
|
createReservationItems(
|
||||||
input: CreateReservationItemInput[],
|
input: CreateReservationItemInput[],
|
||||||
context?: SharedContext
|
context?: SharedContext
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export interface FilterableProductVariantProps
|
|||||||
extends BaseFilterable<FilterableProductVariantProps> {
|
extends BaseFilterable<FilterableProductVariantProps> {
|
||||||
id?: string | string[]
|
id?: string | string[]
|
||||||
sku?: string | string[]
|
sku?: string | string[]
|
||||||
|
product_id?: string | string[]
|
||||||
options?: { id?: string[] }
|
options?: { id?: string[] }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +220,28 @@ export interface CreateProductVariantDTO {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateProductVariantDTO {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
sku?: string
|
||||||
|
barcode?: string
|
||||||
|
ean?: string
|
||||||
|
upc?: string
|
||||||
|
allow_backorder?: boolean
|
||||||
|
inventory_quantity?: number
|
||||||
|
manage_inventory?: boolean
|
||||||
|
hs_code?: string
|
||||||
|
origin_country?: string
|
||||||
|
mid_code?: string
|
||||||
|
material?: string
|
||||||
|
weight?: number
|
||||||
|
length?: number
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
options?: CreateProductVariantOptionDTO[]
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateProductDTO {
|
export interface CreateProductDTO {
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
@@ -233,7 +256,6 @@ export interface CreateProductDTO {
|
|||||||
type_id?: string
|
type_id?: string
|
||||||
collection_id?: string
|
collection_id?: string
|
||||||
tags?: CreateProductTagDTO[]
|
tags?: CreateProductTagDTO[]
|
||||||
// sales_channel
|
|
||||||
categories?: { id: string }[]
|
categories?: { id: string }[]
|
||||||
options?: CreateProductOptionDTO[]
|
options?: CreateProductOptionDTO[]
|
||||||
variants?: CreateProductVariantDTO[]
|
variants?: CreateProductVariantDTO[]
|
||||||
@@ -248,6 +270,35 @@ export interface CreateProductDTO {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateProductDTO {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
description?: string
|
||||||
|
is_giftcard?: boolean
|
||||||
|
discountable?: boolean
|
||||||
|
images?: string[] | { id?: string; url: string }[]
|
||||||
|
thumbnail?: string
|
||||||
|
handle?: string
|
||||||
|
status?: ProductStatus
|
||||||
|
type?: CreateProductTypeDTO
|
||||||
|
type_id?: string | null
|
||||||
|
collection_id?: string | null
|
||||||
|
tags?: CreateProductTagDTO[]
|
||||||
|
categories?: { id: string }[]
|
||||||
|
options?: CreateProductOptionDTO[]
|
||||||
|
variants?: (CreateProductVariantDTO | UpdateProductVariantDTO)[]
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
length?: number
|
||||||
|
weight?: number
|
||||||
|
origin_country?: string
|
||||||
|
hs_code?: string
|
||||||
|
material?: string
|
||||||
|
mid_code?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateProductOnlyDTO {
|
export interface CreateProductOnlyDTO {
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
@@ -294,6 +345,28 @@ export interface CreateProductVariantOnlyDTO {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateProductVariantOnlyDTO {
|
||||||
|
id: string,
|
||||||
|
title?: string
|
||||||
|
sku?: string
|
||||||
|
barcode?: string
|
||||||
|
ean?: string
|
||||||
|
upc?: string
|
||||||
|
allow_backorder?: boolean
|
||||||
|
inventory_quantity?: number
|
||||||
|
manage_inventory?: boolean
|
||||||
|
hs_code?: string
|
||||||
|
origin_country?: string
|
||||||
|
mid_code?: string
|
||||||
|
material?: string
|
||||||
|
weight?: number
|
||||||
|
length?: number
|
||||||
|
height?: number
|
||||||
|
width?: number
|
||||||
|
options?: (CreateProductVariantOptionDTO & { option: any })[]
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateProductOptionOnlyDTO {
|
export interface CreateProductOptionOnlyDTO {
|
||||||
product: { id: string }
|
product: { id: string }
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
CreateProductDTO,
|
CreateProductDTO,
|
||||||
|
UpdateProductDTO,
|
||||||
FilterableProductCategoryProps,
|
FilterableProductCategoryProps,
|
||||||
FilterableProductCollectionProps,
|
FilterableProductCollectionProps,
|
||||||
FilterableProductProps,
|
FilterableProductProps,
|
||||||
@@ -19,7 +20,11 @@ import { JoinerServiceConfig } from "../joiner"
|
|||||||
export interface IProductModuleService {
|
export interface IProductModuleService {
|
||||||
__joinerConfig(): JoinerServiceConfig
|
__joinerConfig(): JoinerServiceConfig
|
||||||
|
|
||||||
retrieve(productId: string, sharedContext?: Context): Promise<ProductDTO>
|
retrieve(
|
||||||
|
productId: string,
|
||||||
|
config?: FindConfig<ProductDTO>,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<ProductDTO>
|
||||||
|
|
||||||
list(
|
list(
|
||||||
filters?: FilterableProductProps,
|
filters?: FilterableProductProps,
|
||||||
@@ -98,6 +103,11 @@ export interface IProductModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<ProductDTO[]>
|
): Promise<ProductDTO[]>
|
||||||
|
|
||||||
|
update(
|
||||||
|
data: UpdateProductDTO[],
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<ProductDTO[]>
|
||||||
|
|
||||||
delete(productIds: string[], sharedContext?: Context): Promise<void>
|
delete(productIds: string[], sharedContext?: Context): Promise<void>
|
||||||
|
|
||||||
softDelete(
|
softDelete(
|
||||||
|
|||||||
Reference in new issue
Block a user