feat(medusa): Allow to assign a Collection to a Product on import (#2764)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: Allow to assign a collection to a product during the import
|
||||||
@@ -8,6 +8,7 @@ const adminSeeder = require("../../../helpers/admin-seeder")
|
|||||||
const userSeeder = require("../../../helpers/user-seeder")
|
const userSeeder = require("../../../helpers/user-seeder")
|
||||||
const { simpleSalesChannelFactory } = require("../../../factories")
|
const { simpleSalesChannelFactory } = require("../../../factories")
|
||||||
const batchJobSeeder = require("../../../helpers/batch-job-seeder")
|
const batchJobSeeder = require("../../../helpers/batch-job-seeder")
|
||||||
|
const { simpleProductCollectionFactory } = require("../../../factories/simple-product-collection-factory");
|
||||||
|
|
||||||
const startServerWithEnvironment =
|
const startServerWithEnvironment =
|
||||||
require("../../../../helpers/start-server-with-environment").default
|
require("../../../../helpers/start-server-with-environment").default
|
||||||
@@ -51,6 +52,8 @@ describe("Product import - Sales Channel", () => {
|
|||||||
let dbConnection
|
let dbConnection
|
||||||
let medusaProcess
|
let medusaProcess
|
||||||
|
|
||||||
|
let collectionHandle1 = "test-collection1"
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
|
||||||
@@ -86,6 +89,9 @@ describe("Product import - Sales Channel", () => {
|
|||||||
await simpleSalesChannelFactory(dbConnection, {
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
name: "Import Sales Channel 2",
|
name: "Import Sales Channel 2",
|
||||||
})
|
})
|
||||||
|
await simpleProductCollectionFactory(dbConnection, {
|
||||||
|
handle: collectionHandle1
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
throw e
|
throw e
|
||||||
@@ -162,6 +168,9 @@ describe("Product import - Sales Channel", () => {
|
|||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
collection: expect.objectContaining({
|
||||||
|
handle: collectionHandle1
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const adminSeeder = require("../../../helpers/admin-seeder")
|
|||||||
const batchJobSeeder = require("../../../helpers/batch-job-seeder")
|
const batchJobSeeder = require("../../../helpers/batch-job-seeder")
|
||||||
const userSeeder = require("../../../helpers/user-seeder")
|
const userSeeder = require("../../../helpers/user-seeder")
|
||||||
const { simpleProductFactory } = require("../../../factories")
|
const { simpleProductFactory } = require("../../../factories")
|
||||||
|
const { simpleProductCollectionFactory } = require("../../../factories/simple-product-collection-factory");
|
||||||
|
|
||||||
const adminReqConfig = {
|
const adminReqConfig = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -49,6 +50,9 @@ describe("Product import batch job", () => {
|
|||||||
let medusaProcess
|
let medusaProcess
|
||||||
let dbConnection
|
let dbConnection
|
||||||
|
|
||||||
|
let collectionHandle1 = "test-collection1"
|
||||||
|
let collectionHandle2 = "test-collection2"
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
dbConnection = await initDb({ cwd })
|
dbConnection = await initDb({ cwd })
|
||||||
@@ -72,14 +76,14 @@ describe("Product import batch job", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
try {
|
await batchJobSeeder(dbConnection)
|
||||||
await batchJobSeeder(dbConnection)
|
await adminSeeder(dbConnection)
|
||||||
await adminSeeder(dbConnection)
|
await userSeeder(dbConnection)
|
||||||
await userSeeder(dbConnection)
|
await simpleProductCollectionFactory(dbConnection, [{
|
||||||
} catch (e) {
|
handle: collectionHandle1
|
||||||
console.log(e)
|
}, {
|
||||||
throw e
|
handle: collectionHandle2
|
||||||
}
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -220,6 +224,9 @@ describe("Product import batch job", () => {
|
|||||||
value: "123_1",
|
value: "123_1",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
collection: expect.objectContaining({
|
||||||
|
handle: collectionHandle1,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "Test product",
|
title: "Test product",
|
||||||
@@ -279,6 +286,9 @@ describe("Product import batch job", () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
collection: expect.objectContaining({
|
||||||
|
handle: collectionHandle1,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
// UPDATED PRODUCT
|
// UPDATED PRODUCT
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -373,6 +383,9 @@ describe("Product import batch job", () => {
|
|||||||
value: "123",
|
value: "123",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
collection: expect.objectContaining({
|
||||||
|
handle: collectionHandle2
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Connection } from "typeorm"
|
||||||
|
import faker from "faker"
|
||||||
|
import { ProductCollection } from "@medusajs/medusa"
|
||||||
|
|
||||||
|
export type Data = {
|
||||||
|
title?: string,
|
||||||
|
handle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const simpleProductCollectionFactory = async <
|
||||||
|
TData extends Data | Data[] = Data | Data[],
|
||||||
|
TResult = TData extends Array<Data> ? ProductCollection[] : ProductCollection
|
||||||
|
>(
|
||||||
|
connection: Connection,
|
||||||
|
data?: TData,
|
||||||
|
seed?: number
|
||||||
|
): Promise<TResult> => {
|
||||||
|
if (typeof seed !== "undefined") {
|
||||||
|
faker.seed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = connection.manager
|
||||||
|
|
||||||
|
data = data || [{
|
||||||
|
title: faker.datatype.string(10),
|
||||||
|
}] as TData
|
||||||
|
|
||||||
|
const collectionsData = Array.isArray(data) ? data : [data]
|
||||||
|
|
||||||
|
const collections: ProductCollection[] = []
|
||||||
|
|
||||||
|
for (const collectionData of collectionsData) {
|
||||||
|
const collection_ = manager.create(ProductCollection, {
|
||||||
|
id: `simple-id-${Math.random() * 1000}`,
|
||||||
|
title: collectionData.title ?? faker.datatype.string(10),
|
||||||
|
handle: collectionData.handle
|
||||||
|
})
|
||||||
|
collections.push(collection_)
|
||||||
|
}
|
||||||
|
|
||||||
|
const productCollections = await manager.save(collections)
|
||||||
|
return (Array.isArray(data) ? productCollections : productCollections[0]) as unknown as TResult
|
||||||
|
}
|
||||||
@@ -581,7 +581,7 @@ class ProductService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(rest)) {
|
for (const [key, value] of Object.entries(rest)) {
|
||||||
if (typeof value !== `undefined`) {
|
if (isDefined(value)) {
|
||||||
product[key] = value
|
product[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,7 +504,9 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
|
|||||||
for (const [, { exportDescriptor: columnSchema }] of Object.entries(
|
for (const [, { exportDescriptor: columnSchema }] of Object.entries(
|
||||||
this.columnsDefinition
|
this.columnsDefinition
|
||||||
)) {
|
)) {
|
||||||
if (!columnSchema || "isDynamic" in columnSchema) continue
|
if (!columnSchema || "isDynamic" in columnSchema) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (columnSchema.entityName === "product") {
|
if (columnSchema.entityName === "product") {
|
||||||
const formattedContent = csvCellContentFormatter(
|
const formattedContent = csvCellContentFormatter(
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces"
|
|||||||
import CsvParser from "../../../services/csv-parser"
|
import CsvParser from "../../../services/csv-parser"
|
||||||
import {
|
import {
|
||||||
BatchJobService,
|
BatchJobService,
|
||||||
|
ProductCollectionService,
|
||||||
ProductService,
|
ProductService,
|
||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
RegionService,
|
RegionService,
|
||||||
SalesChannelService,
|
SalesChannelService,
|
||||||
ShippingProfileService,
|
ShippingProfileService,
|
||||||
} from "../../../services"
|
} from "../../../services"
|
||||||
import { CreateProductInput, UpdateProductInput } from "../../../types/product"
|
import { CreateProductInput } from "../../../types/product"
|
||||||
import {
|
import {
|
||||||
CreateProductVariantInput,
|
CreateProductVariantInput,
|
||||||
UpdateProductVariantInput,
|
UpdateProductVariantInput,
|
||||||
@@ -59,6 +60,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
protected readonly regionService_: RegionService
|
protected readonly regionService_: RegionService
|
||||||
protected readonly productService_: ProductService
|
protected readonly productService_: ProductService
|
||||||
protected readonly batchJobService_: BatchJobService
|
protected readonly batchJobService_: BatchJobService
|
||||||
|
protected readonly productCollectionService_: ProductCollectionService
|
||||||
protected readonly salesChannelService_: SalesChannelService
|
protected readonly salesChannelService_: SalesChannelService
|
||||||
protected readonly productVariantService_: ProductVariantService
|
protected readonly productVariantService_: ProductVariantService
|
||||||
protected readonly shippingProfileService_: ShippingProfileService
|
protected readonly shippingProfileService_: ShippingProfileService
|
||||||
@@ -77,6 +79,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
shippingProfileService,
|
shippingProfileService,
|
||||||
regionService,
|
regionService,
|
||||||
fileService,
|
fileService,
|
||||||
|
productCollectionService,
|
||||||
manager,
|
manager,
|
||||||
featureFlagRouter,
|
featureFlagRouter,
|
||||||
}: ProductImportInjectedProps) {
|
}: ProductImportInjectedProps) {
|
||||||
@@ -106,6 +109,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
this.productVariantService_ = productVariantService
|
this.productVariantService_ = productVariantService
|
||||||
this.shippingProfileService_ = shippingProfileService
|
this.shippingProfileService_ = shippingProfileService
|
||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
|
this.productCollectionService_ = productCollectionService
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildTemplate(): Promise<string> {
|
async buildTemplate(): Promise<string> {
|
||||||
@@ -385,15 +389,15 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
|
|
||||||
const productServiceTx =
|
const productServiceTx =
|
||||||
this.productService_.withTransaction(transactionManager)
|
this.productService_.withTransaction(transactionManager)
|
||||||
|
const productCollectionServiceTx =
|
||||||
|
this.productCollectionService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
const isSalesChannelsFeatureOn = this.featureFlagRouter_.isFeatureEnabled(
|
const isSalesChannelsFeatureOn = this.featureFlagRouter_.isFeatureEnabled(
|
||||||
SalesChannelFeatureFlag.key
|
SalesChannelFeatureFlag.key
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const productOp of productOps) {
|
for (const productOp of productOps) {
|
||||||
const productData = transformProductData(
|
const productData = transformProductData(productOp)
|
||||||
productOp
|
|
||||||
) as unknown as CreateProductInput
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isSalesChannelsFeatureOn && productOp["product.sales_channels"]) {
|
if (isSalesChannelsFeatureOn && productOp["product.sales_channels"]) {
|
||||||
@@ -405,7 +409,23 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await productServiceTx.create(productData)
|
if (
|
||||||
|
productOp["product.collection.handle"] != null &&
|
||||||
|
productOp["product.collection.handle"] !== ""
|
||||||
|
) {
|
||||||
|
productData.collection_id = (
|
||||||
|
await productCollectionServiceTx.retrieveByHandle(
|
||||||
|
productOp["product.collection.handle"] as string,
|
||||||
|
{ select: ["id"] }
|
||||||
|
)
|
||||||
|
).id
|
||||||
|
delete productData.collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we should only pass the expected data and should not have to cast the entire object. Here we are passing everything contained in productData
|
||||||
|
await productServiceTx.create(
|
||||||
|
productData as unknown as CreateProductInput
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
|
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
|
||||||
}
|
}
|
||||||
@@ -432,13 +452,15 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
|
|
||||||
const productServiceTx =
|
const productServiceTx =
|
||||||
this.productService_.withTransaction(transactionManager)
|
this.productService_.withTransaction(transactionManager)
|
||||||
|
const productCollectionServiceTx =
|
||||||
|
this.productCollectionService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
const isSalesChannelsFeatureOn = this.featureFlagRouter_.isFeatureEnabled(
|
const isSalesChannelsFeatureOn = this.featureFlagRouter_.isFeatureEnabled(
|
||||||
SalesChannelFeatureFlag.key
|
SalesChannelFeatureFlag.key
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const productOp of productOps) {
|
for (const productOp of productOps) {
|
||||||
const productData = transformProductData(productOp) as UpdateProductInput
|
const productData = transformProductData(productOp)
|
||||||
try {
|
try {
|
||||||
if (isSalesChannelsFeatureOn) {
|
if (isSalesChannelsFeatureOn) {
|
||||||
productData["sales_channels"] = await this.processSalesChannels(
|
productData["sales_channels"] = await this.processSalesChannels(
|
||||||
@@ -451,6 +473,20 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
|
|
||||||
delete productData.options // for now not supported in the update method
|
delete productData.options // for now not supported in the update method
|
||||||
|
|
||||||
|
if (
|
||||||
|
productOp["product.collection.handle"] != null &&
|
||||||
|
productOp["product.collection.handle"] !== ""
|
||||||
|
) {
|
||||||
|
productData.collection_id = (
|
||||||
|
await productCollectionServiceTx.retrieveByHandle(
|
||||||
|
productOp["product.collection.handle"] as string,
|
||||||
|
{ select: ["id"] }
|
||||||
|
)
|
||||||
|
).id
|
||||||
|
delete productData.collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we should only pass the expected data. Here we are passing everything contained in productData
|
||||||
await productServiceTx.update(
|
await productServiceTx.update(
|
||||||
productOp["product.id"] as string,
|
productOp["product.id"] as string,
|
||||||
productData
|
productData
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Selector } from "../../../../types/common"
|
|||||||
import { CsvSchema, CsvSchemaColumn } from "../../../../interfaces/csv-parser"
|
import { CsvSchema, CsvSchemaColumn } from "../../../../interfaces/csv-parser"
|
||||||
import {
|
import {
|
||||||
BatchJobService,
|
BatchJobService,
|
||||||
|
ProductCollectionService,
|
||||||
ProductService,
|
ProductService,
|
||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
RegionService,
|
RegionService,
|
||||||
@@ -80,6 +81,7 @@ export type ProductImportInjectedProps = {
|
|||||||
shippingProfileService: ShippingProfileService
|
shippingProfileService: ShippingProfileService
|
||||||
salesChannelService: SalesChannelService
|
salesChannelService: SalesChannelService
|
||||||
regionService: RegionService
|
regionService: RegionService
|
||||||
|
productCollectionService: ProductCollectionService
|
||||||
fileService: typeof FileService
|
fileService: typeof FileService
|
||||||
|
|
||||||
featureFlagRouter: FlagRouter
|
featureFlagRouter: FlagRouter
|
||||||
|
|||||||
Reference in New Issue
Block a user