feat: Make tags unique, clean up searchability of product entities (#7014)
This commit is contained in:
@@ -658,8 +658,7 @@ medusaIntegrationTestRunner({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Enforce tag uniqueness in product module
|
it("returns a list of products with tags", async () => {
|
||||||
it.skip("returns a list of products with tags", async () => {
|
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/admin/products?tags[]=${baseProduct.tags[0].id}`,
|
`/admin/products?tags[]=${baseProduct.tags[0].id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
@@ -668,26 +667,25 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.products).toHaveLength(2)
|
expect(response.data.products).toHaveLength(2)
|
||||||
expect(response.data.products).toEqual(
|
expect(response.data.products).toEqual(
|
||||||
expect.arrayContaining(
|
expect.arrayContaining([
|
||||||
[
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
id: baseProduct.id,
|
||||||
id: baseProduct.id,
|
tags: expect.arrayContaining([
|
||||||
tags: [
|
expect.objectContaining({ id: baseProduct.tags[0].id }),
|
||||||
expect.objectContaining({ id: baseProduct.tags[0].id }),
|
]),
|
||||||
],
|
}),
|
||||||
}),
|
|
||||||
],
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: publishedProduct.id,
|
id: publishedProduct.id,
|
||||||
// It should be the same tag instance in both products
|
// It should be the same tag instance in both products
|
||||||
tags: [expect.objectContaining({ id: baseProduct.tags[0].id })],
|
tags: expect.arrayContaining([
|
||||||
})
|
expect.objectContaining({ id: baseProduct.tags[0].id }),
|
||||||
)
|
]),
|
||||||
|
}),
|
||||||
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Enforce tag uniqueness in product module
|
it("returns a list of products with tags in a collection", async () => {
|
||||||
it.skip("returns a list of products with tags in a collection", async () => {
|
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/admin/products?collection_id[]=${baseCollection.id}&tags[]=${baseProduct.tags[0].id}`,
|
`/admin/products?collection_id[]=${baseCollection.id}&tags[]=${baseProduct.tags[0].id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
@@ -700,7 +698,9 @@ medusaIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: baseProduct.id,
|
id: baseProduct.id,
|
||||||
collection_id: baseCollection.id,
|
collection_id: baseCollection.id,
|
||||||
tags: [expect.objectContaining({ id: baseProduct.tags[0].id })],
|
tags: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: baseProduct.tags[0].id }),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -2655,8 +2655,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response2.data.id).toEqual(res.data.product.id)
|
expect(response2.data.id).toEqual(res.data.product.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: We just need to return the correct error message
|
it("should fail when creating a product with a handle that already exists", async () => {
|
||||||
it.skip("should fail when creating a product with a handle that already exists", async () => {
|
|
||||||
// Lets try to create a product with same handle as deleted one
|
// Lets try to create a product with same handle as deleted one
|
||||||
const payload = {
|
const payload = {
|
||||||
title: baseProduct.title,
|
title: baseProduct.title,
|
||||||
@@ -2675,7 +2674,10 @@ medusaIntegrationTestRunner({
|
|||||||
await api.post("/admin/products", payload, adminHeaders)
|
await api.post("/admin/products", payload, adminHeaders)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.response.data.message).toMatch(
|
expect(error.response.data.message).toMatch(
|
||||||
"Product with handle test-product already exists."
|
breaking(
|
||||||
|
() => "Product with handle base-product already exists.",
|
||||||
|
() => "Product with handle: base-product already exists."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const AdminGetProductOptionsParams = createFindParams({
|
|||||||
limit: 50,
|
limit: 50,
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
q: z.string().optional(),
|
||||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
$and: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
$and: z.lazy(() => AdminGetProductsParams.array()).optional(),
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const productsData = [
|
|||||||
tags: [
|
tags: [
|
||||||
{
|
{
|
||||||
id: "tag-3",
|
id: "tag-3",
|
||||||
value: "Germany",
|
value: "Netherlands",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
@@ -825,6 +825,7 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
const productTwoData = buildProductAndRelationsData({
|
const productTwoData = buildProductAndRelationsData({
|
||||||
collection_id: productCollectionTwo.id,
|
collection_id: productCollectionTwo.id,
|
||||||
|
tags: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
await service.create([productOneData, productTwoData])
|
await service.create([productOneData, productTwoData])
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
const productOptions = await service.list(
|
const productOptions = await service.list(
|
||||||
{
|
{
|
||||||
title: "US%",
|
q: "US%",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
relations: ["product"],
|
relations: ["product"],
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ moduleIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("list product tags by value matching string", async () => {
|
it("list product tags by value matching string", async () => {
|
||||||
const tagsResults = await service.list({ value: "united kingdom" })
|
const tagsResults = await service.list({ q: "united kingdom" })
|
||||||
|
|
||||||
expect(tagsResults).toEqual([
|
expect(tagsResults).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { SqlEntityManager } from "@mikro-orm/postgresql"
|
|||||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||||
|
import { ProductTag } from "../../../../src/models"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ export class InitialSetup20240315083440 extends Migration {
|
|||||||
|
|
||||||
this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
this.addSql('create table if not exists "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
||||||
|
|
||||||
// TODO: We need to modify upsertWithReplace to handle unique constraints
|
this.addSql('create unique index if not exists "IDX_tag_value_unique" on "product_tag" (value) where deleted_at is null;')
|
||||||
// this.addSql('create unique index if not exists "IDX_tag_value_unique" on "product_tag" (value) where deleted_at is null;')
|
|
||||||
this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");');
|
this.addSql('create index if not exists "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");');
|
||||||
|
|
||||||
this.addSql('create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));');
|
this.addSql('create table if not exists "product_type" ("id" text not null, "value" text not null, "metadata" json null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
DALUtils,
|
DALUtils,
|
||||||
|
Searchable,
|
||||||
createPsqlIndexStatementHelper,
|
createPsqlIndexStatementHelper,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -35,6 +36,7 @@ class ProductOption {
|
|||||||
@PrimaryKey({ columnType: "text" })
|
@PrimaryKey({ columnType: "text" })
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DALUtils,
|
DALUtils,
|
||||||
|
Searchable,
|
||||||
createPsqlIndexStatementHelper,
|
createPsqlIndexStatementHelper,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -33,6 +34,7 @@ class ProductTag {
|
|||||||
@PrimaryKey({ columnType: "text" })
|
@PrimaryKey({ columnType: "text" })
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DALUtils,
|
DALUtils,
|
||||||
|
Searchable,
|
||||||
createPsqlIndexStatementHelper,
|
createPsqlIndexStatementHelper,
|
||||||
generateEntityId,
|
generateEntityId,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -30,6 +31,7 @@ class ProductType {
|
|||||||
@PrimaryKey({ columnType: "text" })
|
@PrimaryKey({ columnType: "text" })
|
||||||
id!: string
|
id!: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
|
|||||||
@@ -85,12 +85,15 @@ class ProductVariant {
|
|||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
sku?: string | null
|
sku?: string | null
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
barcode?: string | null
|
barcode?: string | null
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
ean?: string | null
|
ean?: string | null
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
upc?: string | null
|
upc?: string | null
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class Product {
|
|||||||
@Property({ columnType: "text" })
|
@Property({ columnType: "text" })
|
||||||
handle?: string
|
handle?: string
|
||||||
|
|
||||||
|
@Searchable()
|
||||||
@Property({ columnType: "text", nullable: true })
|
@Property({ columnType: "text", nullable: true })
|
||||||
subtitle?: string | null
|
subtitle?: string | null
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
export { default as ProductService } from "./product"
|
export { default as ProductService } from "./product"
|
||||||
export { default as ProductCategoryService } from "./product-category"
|
export { default as ProductCategoryService } from "./product-category"
|
||||||
export { default as ProductCollectionService } from "./product-collection"
|
|
||||||
export { default as ProductModuleService } from "./product-module-service"
|
export { default as ProductModuleService } from "./product-module-service"
|
||||||
export { default as ProductTagService } from "./product-tag"
|
|
||||||
export { default as ProductTypeService } from "./product-type"
|
|
||||||
export { default as ProductOptionService } from "./product-option"
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
|
||||||
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
|
|
||||||
|
|
||||||
import { ProductCollection } from "@models"
|
|
||||||
|
|
||||||
type InjectedDependencies = {
|
|
||||||
productCollectionRepository: DAL.RepositoryService
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ProductCollectionService<
|
|
||||||
TEntity extends ProductCollection = ProductCollection
|
|
||||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
|
||||||
ProductCollection
|
|
||||||
)<TEntity> {
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
protected readonly productCollectionRepository_: DAL.RepositoryService<TEntity>
|
|
||||||
|
|
||||||
constructor(container: InjectedDependencies) {
|
|
||||||
super(container)
|
|
||||||
this.productCollectionRepository_ = container.productCollectionRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productCollectionRepository_")
|
|
||||||
async list(
|
|
||||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<TEntity[]> {
|
|
||||||
return await this.productCollectionRepository_.find(
|
|
||||||
this.buildListQueryOptions(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productCollectionRepository_")
|
|
||||||
async listAndCount(
|
|
||||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<[TEntity[], number]> {
|
|
||||||
return await this.productCollectionRepository_.findAndCount(
|
|
||||||
this.buildListQueryOptions(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected buildListQueryOptions(
|
|
||||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
|
||||||
config: FindConfig<TEntity> = {}
|
|
||||||
): DAL.FindOptions<TEntity> {
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<TEntity>(filters, config)
|
|
||||||
|
|
||||||
queryOptions.where ??= {}
|
|
||||||
|
|
||||||
if (filters.title) {
|
|
||||||
queryOptions.where.title = {
|
|
||||||
$like: `%${filters.title}%`,
|
|
||||||
} as DAL.FindOptions<TEntity>["where"]["title"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,14 +18,7 @@ import {
|
|||||||
ProductType,
|
ProductType,
|
||||||
ProductVariant,
|
ProductVariant,
|
||||||
} from "@models"
|
} from "@models"
|
||||||
import {
|
import { ProductCategoryService, ProductService } from "@services"
|
||||||
ProductCategoryService,
|
|
||||||
ProductCollectionService,
|
|
||||||
ProductOptionService,
|
|
||||||
ProductService,
|
|
||||||
ProductTagService,
|
|
||||||
ProductTypeService,
|
|
||||||
} from "@services"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
arrayDifference,
|
arrayDifference,
|
||||||
@@ -58,12 +51,12 @@ type InjectedDependencies = {
|
|||||||
baseRepository: DAL.RepositoryService
|
baseRepository: DAL.RepositoryService
|
||||||
productService: ProductService<any>
|
productService: ProductService<any>
|
||||||
productVariantService: ModulesSdkTypes.InternalModuleService<any, any>
|
productVariantService: ModulesSdkTypes.InternalModuleService<any, any>
|
||||||
productTagService: ProductTagService<any>
|
productTagService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
productCategoryService: ProductCategoryService<any>
|
productCategoryService: ProductCategoryService<any>
|
||||||
productCollectionService: ProductCollectionService<any>
|
productCollectionService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
productImageService: ModulesSdkTypes.InternalModuleService<any>
|
productImageService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
productTypeService: ProductTypeService<any>
|
productTypeService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
productOptionService: ProductOptionService<any>
|
productOptionService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
productOptionValueService: ModulesSdkTypes.InternalModuleService<any>
|
productOptionValueService: ModulesSdkTypes.InternalModuleService<any>
|
||||||
eventBusModuleService?: IEventBusModuleService
|
eventBusModuleService?: IEventBusModuleService
|
||||||
}
|
}
|
||||||
@@ -133,13 +126,13 @@ export default class ProductModuleService<
|
|||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productCategoryService_: ProductCategoryService<TProductCategory>
|
protected readonly productCategoryService_: ProductCategoryService<TProductCategory>
|
||||||
protected readonly productTagService_: ProductTagService<TProductTag>
|
protected readonly productTagService_: ModulesSdkTypes.InternalModuleService<TProductTag>
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productCollectionService_: ProductCollectionService<TProductCollection>
|
protected readonly productCollectionService_: ModulesSdkTypes.InternalModuleService<TProductCollection>
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productImageService_: ModulesSdkTypes.InternalModuleService<TProductImage>
|
protected readonly productImageService_: ModulesSdkTypes.InternalModuleService<TProductImage>
|
||||||
protected readonly productTypeService_: ProductTypeService<TProductType>
|
protected readonly productTypeService_: ModulesSdkTypes.InternalModuleService<TProductType>
|
||||||
protected readonly productOptionService_: ProductOptionService<TProductOption>
|
protected readonly productOptionService_: ModulesSdkTypes.InternalModuleService<TProductOption>
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productOptionValueService_: ModulesSdkTypes.InternalModuleService<TProductOptionValue>
|
protected readonly productOptionValueService_: ModulesSdkTypes.InternalModuleService<TProductOptionValue>
|
||||||
protected readonly eventBusModuleService_?: IEventBusModuleService
|
protected readonly eventBusModuleService_?: IEventBusModuleService
|
||||||
@@ -1122,8 +1115,8 @@ export default class ProductModuleService<
|
|||||||
data: ProductTypes.CreateProductDTO[],
|
data: ProductTypes.CreateProductDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TProduct[]> {
|
): Promise<TProduct[]> {
|
||||||
const normalizedInput = data.map(
|
const normalizedInput = await Promise.all(
|
||||||
ProductModuleService.normalizeCreateProductInput
|
data.map((d) => this.normalizeCreateProductInput(d, sharedContext))
|
||||||
)
|
)
|
||||||
|
|
||||||
const productData = await this.productService_.upsertWithReplace(
|
const productData = await this.productService_.upsertWithReplace(
|
||||||
@@ -1178,8 +1171,8 @@ export default class ProductModuleService<
|
|||||||
data: UpdateProductInput[],
|
data: UpdateProductInput[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TProduct[]> {
|
): Promise<TProduct[]> {
|
||||||
const normalizedInput = data.map(
|
const normalizedInput = await Promise.all(
|
||||||
ProductModuleService.normalizeUpdateProductInput
|
data.map((d) => this.normalizeUpdateProductInput(d, sharedContext))
|
||||||
)
|
)
|
||||||
|
|
||||||
const productData = await this.productService_.upsertWithReplace(
|
const productData = await this.productService_.upsertWithReplace(
|
||||||
@@ -1260,12 +1253,13 @@ export default class ProductModuleService<
|
|||||||
return productData
|
return productData
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static normalizeCreateProductInput(
|
protected async normalizeCreateProductInput(
|
||||||
product: ProductTypes.CreateProductDTO
|
product: ProductTypes.CreateProductDTO,
|
||||||
): ProductTypes.CreateProductDTO {
|
@MedusaContext() sharedContext: Context = {}
|
||||||
const productData = ProductModuleService.normalizeUpdateProductInput(
|
): Promise<ProductTypes.CreateProductDTO> {
|
||||||
|
const productData = (await this.normalizeUpdateProductInput(
|
||||||
product as UpdateProductInput
|
product as UpdateProductInput
|
||||||
) as ProductTypes.CreateProductDTO
|
)) as ProductTypes.CreateProductDTO
|
||||||
|
|
||||||
if (!productData.handle && productData.title) {
|
if (!productData.handle && productData.title) {
|
||||||
productData.handle = kebabCase(productData.title)
|
productData.handle = kebabCase(productData.title)
|
||||||
@@ -1282,14 +1276,35 @@ export default class ProductModuleService<
|
|||||||
return productData
|
return productData
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static normalizeUpdateProductInput(
|
protected async normalizeUpdateProductInput(
|
||||||
product: UpdateProductInput
|
product: UpdateProductInput,
|
||||||
): UpdateProductInput {
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<UpdateProductInput> {
|
||||||
const productData = { ...product }
|
const productData = { ...product }
|
||||||
if (productData.is_giftcard) {
|
if (productData.is_giftcard) {
|
||||||
productData.discountable = false
|
productData.discountable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (productData.tags?.length && productData.tags.some((t) => !t.id)) {
|
||||||
|
const dbTags = await this.productTagService_.list(
|
||||||
|
{
|
||||||
|
value: productData.tags
|
||||||
|
.map((t) => t.value)
|
||||||
|
.filter((v) => !!v) as string[],
|
||||||
|
},
|
||||||
|
{ take: null },
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
productData.tags = productData.tags.map((tag) => {
|
||||||
|
const dbTag = dbTags.find((t) => t.value === tag.value)
|
||||||
|
return {
|
||||||
|
...tag,
|
||||||
|
...(dbTag ? { id: dbTag.id } : {}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (productData.options?.length) {
|
if (productData.options?.length) {
|
||||||
;(productData as any).options = productData.options?.map((option) => {
|
;(productData as any).options = productData.options?.map((option) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { ProductOption } from "@models"
|
|
||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
|
||||||
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
|
|
||||||
|
|
||||||
type InjectedDependencies = {
|
|
||||||
productOptionRepository: DAL.RepositoryService
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ProductOptionService<
|
|
||||||
TEntity extends ProductOption = ProductOption
|
|
||||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
|
||||||
ProductOption
|
|
||||||
)<TEntity> {
|
|
||||||
protected readonly productOptionRepository_: DAL.RepositoryService<TEntity>
|
|
||||||
|
|
||||||
constructor(container: InjectedDependencies) {
|
|
||||||
super(container)
|
|
||||||
this.productOptionRepository_ = container.productOptionRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productOptionRepository_")
|
|
||||||
async list(
|
|
||||||
filters: ProductTypes.FilterableProductOptionProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext?: Context
|
|
||||||
): Promise<TEntity[]> {
|
|
||||||
return await this.productOptionRepository_.find(
|
|
||||||
this.buildQueryForList(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productOptionRepository_")
|
|
||||||
async listAndCount(
|
|
||||||
filters: ProductTypes.FilterableProductOptionProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext?: Context
|
|
||||||
): Promise<[TEntity[], number]> {
|
|
||||||
return await this.productOptionRepository_.findAndCount(
|
|
||||||
this.buildQueryForList(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildQueryForList(
|
|
||||||
filters: ProductTypes.FilterableProductOptionProps = {},
|
|
||||||
config: FindConfig<TEntity> = {}
|
|
||||||
): DAL.FindOptions<TEntity> {
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<TEntity>(filters, config)
|
|
||||||
|
|
||||||
if (filters.title) {
|
|
||||||
queryOptions.where.title = {
|
|
||||||
$ilike: filters.title,
|
|
||||||
} as DAL.FindOptions<TEntity>["where"]["title"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { ProductTag } from "@models"
|
|
||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
|
||||||
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
|
|
||||||
|
|
||||||
type InjectedDependencies = {
|
|
||||||
productTagRepository: DAL.RepositoryService
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ProductTagService<
|
|
||||||
TEntity extends ProductTag = ProductTag
|
|
||||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
|
||||||
ProductTag
|
|
||||||
)<TEntity> {
|
|
||||||
protected readonly productTagRepository_: DAL.RepositoryService<TEntity>
|
|
||||||
|
|
||||||
constructor(container: InjectedDependencies) {
|
|
||||||
super(container)
|
|
||||||
this.productTagRepository_ = container.productTagRepository
|
|
||||||
}
|
|
||||||
@InjectManager("productTagRepository_")
|
|
||||||
async list(
|
|
||||||
filters: ProductTypes.FilterableProductTagProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<TEntity[]> {
|
|
||||||
return await this.productTagRepository_.find(
|
|
||||||
this.buildQueryForList(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productTagRepository_")
|
|
||||||
async listAndCount(
|
|
||||||
filters: ProductTypes.FilterableProductTagProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<[TEntity[], number]> {
|
|
||||||
return await this.productTagRepository_.findAndCount(
|
|
||||||
this.buildQueryForList(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildQueryForList(
|
|
||||||
filters: ProductTypes.FilterableProductTagProps = {},
|
|
||||||
config: FindConfig<TEntity> = {}
|
|
||||||
): DAL.FindOptions<TEntity> {
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<TEntity>(filters, config)
|
|
||||||
|
|
||||||
if (filters.value) {
|
|
||||||
queryOptions.where.value = {
|
|
||||||
$ilike: filters.value,
|
|
||||||
} as DAL.FindOptions<TEntity>["where"]["value"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { ProductType } from "@models"
|
|
||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
|
||||||
import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils"
|
|
||||||
|
|
||||||
type InjectedDependencies = {
|
|
||||||
productTypeRepository: DAL.RepositoryService
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ProductTypeService<
|
|
||||||
TEntity extends ProductType = ProductType
|
|
||||||
> extends ModulesSdkUtils.internalModuleServiceFactory<InjectedDependencies>(
|
|
||||||
ProductType
|
|
||||||
)<TEntity> {
|
|
||||||
protected readonly productTypeRepository_: DAL.RepositoryService<TEntity>
|
|
||||||
|
|
||||||
constructor(container: InjectedDependencies) {
|
|
||||||
super(container)
|
|
||||||
this.productTypeRepository_ = container.productTypeRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productTypeRepository_")
|
|
||||||
async list(
|
|
||||||
filters: ProductTypes.FilterableProductTypeProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<TEntity[]> {
|
|
||||||
return await this.productTypeRepository_.find(
|
|
||||||
this.buildQueryForList(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@InjectManager("productTypeRepository_")
|
|
||||||
async listAndCount(
|
|
||||||
filters: ProductTypes.FilterableProductTypeProps = {},
|
|
||||||
config: FindConfig<TEntity> = {},
|
|
||||||
@MedusaContext() sharedContext: Context = {}
|
|
||||||
): Promise<[TEntity[], number]> {
|
|
||||||
return await this.productTypeRepository_.findAndCount(
|
|
||||||
this.buildQueryForList(filters, config),
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildQueryForList(
|
|
||||||
filters: ProductTypes.FilterableProductTypeProps = {},
|
|
||||||
config: FindConfig<TEntity> = {}
|
|
||||||
): DAL.FindOptions<TEntity> {
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<TEntity>(filters, config)
|
|
||||||
|
|
||||||
if (filters.value) {
|
|
||||||
queryOptions.where.value = {
|
|
||||||
$ilike: filters.value,
|
|
||||||
} as DAL.FindOptions<TEntity>["where"]["value"]
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryOptions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -724,6 +724,10 @@ export interface FilterableProductProps
|
|||||||
*/
|
*/
|
||||||
export interface FilterableProductTagProps
|
export interface FilterableProductTagProps
|
||||||
extends BaseFilterable<FilterableProductTagProps> {
|
extends BaseFilterable<FilterableProductTagProps> {
|
||||||
|
/**
|
||||||
|
* Search through the tags' values.
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
/**
|
/**
|
||||||
* The IDs to filter product tags by.
|
* The IDs to filter product tags by.
|
||||||
*/
|
*/
|
||||||
@@ -731,7 +735,7 @@ export interface FilterableProductTagProps
|
|||||||
/**
|
/**
|
||||||
* The value to filter product tags by.
|
* The value to filter product tags by.
|
||||||
*/
|
*/
|
||||||
value?: string
|
value?: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -744,6 +748,10 @@ export interface FilterableProductTagProps
|
|||||||
*/
|
*/
|
||||||
export interface FilterableProductTypeProps
|
export interface FilterableProductTypeProps
|
||||||
extends BaseFilterable<FilterableProductTypeProps> {
|
extends BaseFilterable<FilterableProductTypeProps> {
|
||||||
|
/**
|
||||||
|
* Search through the types' values.
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
/**
|
/**
|
||||||
* The IDs to filter product types by.
|
* The IDs to filter product types by.
|
||||||
*/
|
*/
|
||||||
@@ -765,6 +773,10 @@ export interface FilterableProductTypeProps
|
|||||||
*/
|
*/
|
||||||
export interface FilterableProductOptionProps
|
export interface FilterableProductOptionProps
|
||||||
extends BaseFilterable<FilterableProductOptionProps> {
|
extends BaseFilterable<FilterableProductOptionProps> {
|
||||||
|
/**
|
||||||
|
* Search through the options' titles.
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
/**
|
/**
|
||||||
* The IDs to filter product options by.
|
* The IDs to filter product options by.
|
||||||
*/
|
*/
|
||||||
@@ -789,6 +801,10 @@ export interface FilterableProductOptionProps
|
|||||||
*/
|
*/
|
||||||
export interface FilterableProductCollectionProps
|
export interface FilterableProductCollectionProps
|
||||||
extends BaseFilterable<FilterableProductCollectionProps> {
|
extends BaseFilterable<FilterableProductCollectionProps> {
|
||||||
|
/**
|
||||||
|
* Search through the collections' titles.
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
/**
|
/**
|
||||||
* The IDs to filter product collections by.
|
* The IDs to filter product collections by.
|
||||||
*/
|
*/
|
||||||
@@ -815,6 +831,10 @@ export interface FilterableProductCollectionProps
|
|||||||
*/
|
*/
|
||||||
export interface FilterableProductVariantProps
|
export interface FilterableProductVariantProps
|
||||||
extends BaseFilterable<FilterableProductVariantProps> {
|
extends BaseFilterable<FilterableProductVariantProps> {
|
||||||
|
/**
|
||||||
|
* Search through the title and different code attributes on the variant
|
||||||
|
*/
|
||||||
|
q?: string
|
||||||
/**
|
/**
|
||||||
* The IDs to filter product variants by.
|
* The IDs to filter product variants by.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const dbErrorMapper = (err: Error) => {
|
|||||||
|
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`${upperCaseFirst(info.table)} with ${info.keys
|
`${upperCaseFirst(info.table.split("_").join(" "))} with ${info.keys
|
||||||
.map((key, i) => `${key}: ${info.values[i]}`)
|
.map((key, i) => `${key}: ${info.values[i]}`)
|
||||||
.join(", ")} already exists.`
|
.join(", ")} already exists.`
|
||||||
)
|
)
|
||||||
@@ -37,7 +37,7 @@ export const dbErrorMapper = (err: Error) => {
|
|||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Cannot set field '${(err as any).column}' of ${upperCaseFirst(
|
`Cannot set field '${(err as any).column}' of ${upperCaseFirst(
|
||||||
(err as any).table
|
(err as any).table.split("_").join(" ")
|
||||||
)} to null`
|
)} to null`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user