feat(medusa): Allow to assign a Collection to a Product on import (#2764)

This commit is contained in:
Adrien de Peretti
2022-12-12 17:10:03 +01:00
committed by GitHub
parent 33aa3edb80
commit 424efff919
8 changed files with 126 additions and 16 deletions
+5
View File
@@ -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
}
+1 -1
View File
@@ -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