feat(product): Create (+ workflow), delete, restore (#4459)
* Feat: create product with product module * feat: create product wip * feat: create product wip * feat: update product relation and generate image migration * lint * conitnue implementation * continue implementation and add integration tests for produceService.create * Add integration tests for product creation at the module level for the complete flow * only use persist since write operations are always wrapped in a transaction which will be committed and flushed * simplify the transaction wrapper to make future changes easier * feat: move some utils to the utils package to simplify its usage * tests: fix unit tests * feat: create variants along side the product * Add more integration tests an update migrations * chore: Update actions workflow to include packages integration tests * small types and utils cleanup * chore: Add support for database debug option * chore: Add missing types in package.json from types and util, validate that all the models are sync with medusa * expose retrieve method * fix types issues * fix unit tests and move integration tests workflow with the plugins integration tests * chore: remove migration function export from the definition to prevent them to be ran by the medusa cli just in case * fix package.json script * chore: workflows * feat: start creating the create product workflow * feat: add empty step for prices and sales channel * tests: update scripts and action envs * fix imports * feat: Add proper soft deleted support + add product deletion service public api * chore: update migrations * chore: update migrations * chore: update todo * feat: Add product deletion to the create-product workflow as compensation * chore: cleanup product utils * feat: Add support for cascade soft-remove * feat: refactor repository to take into account withDeleted * fix integration tests * Add support for force delete -> delete, cleanup repositories and improvements * Add support for restoring a product and add integration tests * cleaup + tests * types * fix integration tests * remove unnecessary comments * move specific mikro orm usage to the DAL * Cleanup workflow functions * Make deleted_at optional at the property level and add url index for the images * address feedback + cleanup * fix export * merge migrations into one * feat(product, types): added missing product variant methods (#4475) * chore: added missing product variant methods * chore: address PR feedback * chore: catch undefined case for retrieve + specs for variant service * chore: align TEntity + add changeset * chore: revert changeset, TEntity to ProductVariant * chore: write tests for pagination, unskip the test * Create chilled-mice-deliver.md * update integration fixtuers * update pipeline node version * rename github action * fix pipeline * feat(medusa, types): added missing category tests and service methods (#4499) * chore: added missing category tests and service methods * chore: added type changes to module service * chore: address pr feedback * update repositories manager usage and serialisation from the write public API * move serializisation to the DAL * rename template args * chore: added collection methods for module and collection service (#4505) * chore: added collection methods for module and collection service * Create fresh-islands-teach.md * chore: move retrieve entity to utils package * chore: make products optional in DTO type --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * feat(product): Apply transaction decorators to the services (#4512) --------- Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Oliver Windall Juhl
Riqwan Thamir
Carlos R. L. Rodrigues
parent
5b91a3503a
commit
befc2f1c80
@@ -0,0 +1,159 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import {
|
||||
IInventoryService,
|
||||
MedusaContainer,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { ulid } from "ulid"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import {
|
||||
DistributedTransaction,
|
||||
TransactionHandlerType,
|
||||
TransactionOrchestrator,
|
||||
TransactionPayload,
|
||||
TransactionState,
|
||||
TransactionStepsDefinition,
|
||||
} from "../../../utils/transaction"
|
||||
import { CreateProductVariantInput } from "../../../types/product-variant"
|
||||
import {
|
||||
attachInventoryItems,
|
||||
createInventoryItems,
|
||||
createProducts,
|
||||
removeInventoryItems,
|
||||
removeProducts,
|
||||
} from "../../functions"
|
||||
|
||||
enum Actions {
|
||||
createProduct = "createProduct",
|
||||
createPrices = "createPrices",
|
||||
attachToSalesChannel = "attachToSalesChannel",
|
||||
createInventoryItems = "createInventoryItems",
|
||||
attachInventoryItems = "attachInventoryItems",
|
||||
}
|
||||
|
||||
const workflowSteps: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: Actions.createProduct,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: Actions.attachToSalesChannel,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: Actions.createPrices,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: Actions.createInventoryItems,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: Actions.attachInventoryItems,
|
||||
noCompensation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const createProductOrchestrator = new TransactionOrchestrator(
|
||||
"create-product",
|
||||
workflowSteps
|
||||
)
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
container: MedusaContainer
|
||||
inventoryService?: IInventoryService
|
||||
}
|
||||
|
||||
export async function createProductWorkflow(
|
||||
dependencies: InjectedDependencies,
|
||||
productId: string,
|
||||
input: CreateProductVariantInput[]
|
||||
): Promise<DistributedTransaction> {
|
||||
const { manager, container } = dependencies
|
||||
async function transactionHandler(
|
||||
actionId: string,
|
||||
type: TransactionHandlerType,
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
[Actions.createProduct]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: ProductTypes.CreateProductDTO[]
|
||||
) => {
|
||||
return await createProducts({
|
||||
container,
|
||||
data,
|
||||
})
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (
|
||||
data: any[],
|
||||
{ invoke }
|
||||
) => {
|
||||
const createdProducts = invoke[Actions.createProduct]
|
||||
return await removeProducts({ container, data: createdProducts })
|
||||
},
|
||||
},
|
||||
[Actions.createInventoryItems]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput[],
|
||||
{ invoke }
|
||||
) => {
|
||||
const { [Actions.createProduct]: products } = invoke
|
||||
|
||||
return await createInventoryItems({
|
||||
container,
|
||||
manager,
|
||||
data: products,
|
||||
})
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (_, { invoke }) => {
|
||||
const variantInventoryItemsData = invoke[Actions.createInventoryItems]
|
||||
await removeInventoryItems({
|
||||
container,
|
||||
manager,
|
||||
data: variantInventoryItemsData,
|
||||
})
|
||||
},
|
||||
},
|
||||
[Actions.attachInventoryItems]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput[],
|
||||
{ invoke }
|
||||
) => {
|
||||
const { [Actions.createInventoryItems]: inventoryItemsResult } =
|
||||
invoke
|
||||
|
||||
return await attachInventoryItems({
|
||||
container,
|
||||
manager,
|
||||
data: inventoryItemsResult,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
return command[actionId][type](payload.data, payload.context)
|
||||
}
|
||||
|
||||
const orchestrator = createProductOrchestrator
|
||||
|
||||
const transaction = await orchestrator.beginTransaction(
|
||||
ulid(),
|
||||
transactionHandler,
|
||||
input
|
||||
)
|
||||
|
||||
await orchestrator.resume(transaction)
|
||||
|
||||
if (transaction.getState() !== TransactionState.DONE) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
transaction
|
||||
.getErrors()
|
||||
.map((err) => err.error?.message)
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
return transaction
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
InventoryItemDTO,
|
||||
MedusaContainer,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
export async function attachInventoryItems({
|
||||
container,
|
||||
manager,
|
||||
data,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
manager: EntityManager
|
||||
data: {
|
||||
variant: ProductTypes.ProductVariantDTO
|
||||
inventoryItem: InventoryItemDTO
|
||||
}[]
|
||||
}) {
|
||||
const productVariantInventoryService = container
|
||||
.resolve("productVariantInventoryService")
|
||||
.withTransaction(manager)
|
||||
|
||||
return await Promise.all(
|
||||
data
|
||||
.filter((d) => d)
|
||||
.map(async ({ variant, inventoryItem }) => {
|
||||
return await productVariantInventoryService.attachInventoryItem(
|
||||
variant.id,
|
||||
inventoryItem.id
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
IInventoryService,
|
||||
MedusaContainer,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
export async function createInventoryItems({
|
||||
container,
|
||||
manager,
|
||||
data,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
manager: EntityManager
|
||||
data: ProductTypes.ProductDTO[]
|
||||
}) {
|
||||
const inventoryService: IInventoryService =
|
||||
container.resolve("inventoryService")
|
||||
const context = { transactionManager: manager }
|
||||
|
||||
const variants = data.reduce(
|
||||
(
|
||||
acc: ProductTypes.ProductVariantDTO[],
|
||||
product: ProductTypes.ProductDTO
|
||||
) => {
|
||||
return acc.concat(product.variants)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return await Promise.all(
|
||||
variants.map(async (variant) => {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
const inventoryItem = await inventoryService!.createInventoryItem(
|
||||
{
|
||||
sku: variant.sku!,
|
||||
origin_country: variant.origin_country!,
|
||||
hs_code: variant.hs_code!,
|
||||
mid_code: variant.mid_code!,
|
||||
material: variant.material!,
|
||||
weight: variant.weight!,
|
||||
length: variant.length!,
|
||||
height: variant.height!,
|
||||
width: variant.width!,
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
return { variant, inventoryItem }
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MedusaContainer, ProductTypes } from "@medusajs/types"
|
||||
|
||||
export async function removeProducts({
|
||||
container,
|
||||
data,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
data: ProductTypes.ProductDTO[]
|
||||
}): Promise<ProductTypes.ProductDTO[]> {
|
||||
const productModuleService = container.resolve("productModuleService")
|
||||
return await productModuleService.softDelete(data.map((p) => p.id))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./create-prducts"
|
||||
export * from "./remove-products"
|
||||
export * from "./create-inventory-items"
|
||||
export * from "./remove-inventory-items"
|
||||
export * from "./attach-inventory-items"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { InventoryItemDTO, MedusaContainer } from "@medusajs/types"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
export async function removeInventoryItems({
|
||||
container,
|
||||
manager,
|
||||
data,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
manager: EntityManager
|
||||
data: {
|
||||
inventoryItem: InventoryItemDTO
|
||||
}[]
|
||||
}) {
|
||||
const inventoryService = container.resolve("inventoryService")
|
||||
const context = { transactionManager: manager }
|
||||
|
||||
return await Promise.all(
|
||||
data.map(async ({ inventoryItem }) => {
|
||||
return await inventoryService!.deleteInventoryItem(
|
||||
inventoryItem.id,
|
||||
context
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MedusaContainer, ProductTypes } from "@medusajs/types"
|
||||
|
||||
export async function createProducts({
|
||||
container,
|
||||
data,
|
||||
}: {
|
||||
container: MedusaContainer
|
||||
data: ProductTypes.CreateProductDTO[]
|
||||
}) {
|
||||
const productModuleService = container.resolve("productModuleService")
|
||||
return await productModuleService.create(data)
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import { FindOptions, RepositoryService } from "@medusajs/types"
|
||||
import { BaseRepository } from "../../src/repositories/base"
|
||||
|
||||
class CustomRepository implements RepositoryService {
|
||||
constructor() {}
|
||||
|
||||
find(options?: FindOptions): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(options?: FindOptions): Promise<[any[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
class CustomRepository extends BaseRepository {
|
||||
constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import faker from "faker"
|
||||
import { Image } from "@models"
|
||||
|
||||
export const buildProductOnlyData = ({
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
is_giftcard,
|
||||
discountable,
|
||||
thumbnail,
|
||||
images,
|
||||
status,
|
||||
}: {
|
||||
title?: string
|
||||
description?: string
|
||||
subtitle?: string
|
||||
is_giftcard?: boolean
|
||||
discountable?: boolean
|
||||
thumbnail?: string
|
||||
images?: { id?: string; url: string }[]
|
||||
status?: ProductTypes.ProductStatus
|
||||
} = {}) => {
|
||||
return {
|
||||
title: title ?? faker.commerce.productName(),
|
||||
description: description ?? faker.commerce.productName(),
|
||||
subtitle: subtitle ?? faker.commerce.productName(),
|
||||
is_giftcard: is_giftcard ?? false,
|
||||
discountable: discountable ?? true,
|
||||
thumbnail: thumbnail as string,
|
||||
status: status ?? ProductTypes.ProductStatus.PUBLISHED,
|
||||
images: (images ?? []) as Image[],
|
||||
}
|
||||
}
|
||||
|
||||
export const buildProductAndRelationsData = ({
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
is_giftcard,
|
||||
discountable,
|
||||
thumbnail,
|
||||
images,
|
||||
status,
|
||||
type,
|
||||
tags,
|
||||
options,
|
||||
variants,
|
||||
}: Partial<ProductTypes.CreateProductDTO>) => {
|
||||
const defaultOptionTitle = faker.commerce.productName()
|
||||
return {
|
||||
title: title ?? faker.commerce.productName(),
|
||||
description: description ?? faker.commerce.productName(),
|
||||
subtitle: subtitle ?? faker.commerce.productName(),
|
||||
is_giftcard: is_giftcard ?? false,
|
||||
discountable: discountable ?? true,
|
||||
thumbnail: thumbnail as string,
|
||||
status: status ?? ProductTypes.ProductStatus.PUBLISHED,
|
||||
images: (images ?? []) as Image[],
|
||||
type: type ? { value: type } : { value: faker.commerce.productName() },
|
||||
tags: tags ?? [{ value: "tag-1" }],
|
||||
options: options ?? [
|
||||
{
|
||||
title: defaultOptionTitle,
|
||||
},
|
||||
],
|
||||
variants: variants ?? [
|
||||
{
|
||||
title: faker.commerce.productName(),
|
||||
sku: faker.commerce.productName(),
|
||||
options: [
|
||||
{
|
||||
value: defaultOptionTitle + faker.commerce.productName(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// TODO: add categories, must be created first
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
Image,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
@@ -7,9 +9,16 @@ import {
|
||||
} from "@models"
|
||||
import ProductOption from "../../../src/models/product-option"
|
||||
|
||||
export * from "./data/create-product"
|
||||
|
||||
export async function createProductAndTags(
|
||||
manager: SqlEntityManager,
|
||||
data: any[]
|
||||
data: {
|
||||
id?: string
|
||||
title: string
|
||||
status: ProductTypes.ProductStatus
|
||||
tags?: { id: string; value: string }[]
|
||||
}[]
|
||||
) {
|
||||
const products: any[] = data.map((productData) => {
|
||||
return manager.create(Product, productData)
|
||||
@@ -35,7 +44,11 @@ export async function createProductVariants(
|
||||
|
||||
export async function createCollections(
|
||||
manager: SqlEntityManager,
|
||||
collectionData: any[]
|
||||
collectionData: {
|
||||
id?: string
|
||||
title: string
|
||||
handle?: string
|
||||
}[]
|
||||
) {
|
||||
const collections: any[] = collectionData.map((collectionData) => {
|
||||
return manager.create(ProductCollection, collectionData)
|
||||
@@ -48,10 +61,21 @@ export async function createCollections(
|
||||
|
||||
export async function createOptions(
|
||||
manager: SqlEntityManager,
|
||||
optionsData: any[]
|
||||
optionsData: {
|
||||
id?: string
|
||||
product: { id: string }
|
||||
title: string
|
||||
value?: string
|
||||
values?: {
|
||||
id?: string
|
||||
value: string
|
||||
variant?: { id: string } & any
|
||||
}[]
|
||||
variant?: { id: string } & any
|
||||
}[]
|
||||
) {
|
||||
const options: any[] = optionsData.map((o) => {
|
||||
return manager.create(ProductOption, o)
|
||||
const options: any[] = optionsData.map((option) => {
|
||||
return manager.create(ProductOption, option)
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(options)
|
||||
@@ -59,6 +83,19 @@ export async function createOptions(
|
||||
return options
|
||||
}
|
||||
|
||||
export async function createImages(
|
||||
manager: SqlEntityManager,
|
||||
imagesData: string[]
|
||||
) {
|
||||
const images: any[] = imagesData.map((img) => {
|
||||
return manager.create(Image, { url: img })
|
||||
})
|
||||
|
||||
await manager.persistAndFlush(images)
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
export async function assignCategoriesToProduct(
|
||||
manager: SqlEntityManager,
|
||||
product: Product,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
import faker from "faker"
|
||||
|
||||
export const buildProductVariantOnlyData = ({
|
||||
title,
|
||||
sku,
|
||||
barcode,
|
||||
ean,
|
||||
upc,
|
||||
allow_backorder,
|
||||
inventory_quantity,
|
||||
manage_inventory,
|
||||
hs_code,
|
||||
origin_country,
|
||||
mid_code,
|
||||
material,
|
||||
weight,
|
||||
length,
|
||||
height,
|
||||
width,
|
||||
options,
|
||||
metadata,
|
||||
}: Partial<ProductTypes.CreateProductVariantOnlyDTO>) => {
|
||||
return {
|
||||
title: title ?? faker.commerce.productName(),
|
||||
sku: sku ?? faker.commerce.productName(),
|
||||
barcode,
|
||||
ean,
|
||||
upc,
|
||||
allow_backorder,
|
||||
inventory_quantity,
|
||||
manage_inventory,
|
||||
hs_code,
|
||||
origin_country,
|
||||
mid_code,
|
||||
material,
|
||||
weight,
|
||||
length,
|
||||
height,
|
||||
width,
|
||||
options,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./data/create-variant"
|
||||
@@ -1,11 +1,13 @@
|
||||
import { MedusaModule } from "@medusajs/modules-sdk"
|
||||
import { Product } from "@models"
|
||||
import { initialize } from "../../src"
|
||||
import * as CustomRepositories from "../__fixtures__/module"
|
||||
import { ProductRepository } from "../__fixtures__/module"
|
||||
import { createProductAndTags } from "../__fixtures__/product"
|
||||
import { productsData } from "../__fixtures__/product/data"
|
||||
import { DB_URL, TestDatabase } from "../utils"
|
||||
import { buildProductAndRelationsData } from "../__fixtures__/product/data/create-product"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
|
||||
const beforeEach_ = async () => {
|
||||
await TestDatabase.setupDatabase()
|
||||
@@ -18,12 +20,11 @@ const afterEach_ = async () => {
|
||||
|
||||
describe("Product module", function () {
|
||||
describe("Using built-in data access layer", function () {
|
||||
let module
|
||||
let products: Product[]
|
||||
let module: IProductModuleService
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await beforeEach_()
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
await createProductAndTags(testManager, productsData)
|
||||
|
||||
module = await initialize({
|
||||
database: {
|
||||
@@ -46,13 +47,12 @@ describe("Product module", function () {
|
||||
})
|
||||
|
||||
describe("Using custom data access layer", function () {
|
||||
let module
|
||||
let products: Product[]
|
||||
let module: IProductModuleService
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await beforeEach_()
|
||||
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
await createProductAndTags(testManager, productsData)
|
||||
|
||||
module = await initialize({
|
||||
database: {
|
||||
@@ -78,12 +78,11 @@ describe("Product module", function () {
|
||||
})
|
||||
|
||||
describe("Using custom data access layer and connection", function () {
|
||||
let module
|
||||
let products: Product[]
|
||||
let module: IProductModuleService
|
||||
|
||||
beforeEach(async () => {
|
||||
const testManager = await beforeEach_()
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
await createProductAndTags(testManager, productsData)
|
||||
|
||||
MedusaModule.clearInstances()
|
||||
|
||||
@@ -106,4 +105,244 @@ describe("Product module", function () {
|
||||
expect(products).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let module: IProductModuleService
|
||||
let images = ["image-1"]
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEach_()
|
||||
|
||||
MedusaModule.clearInstances()
|
||||
|
||||
module = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(afterEach_)
|
||||
|
||||
it("should create a product", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0],
|
||||
})
|
||||
|
||||
const products = await module.create([data])
|
||||
|
||||
expect(products).toHaveLength(1)
|
||||
|
||||
expect(products[0].images).toHaveLength(1)
|
||||
expect(products[0].options).toHaveLength(1)
|
||||
expect(products[0].tags).toHaveLength(1)
|
||||
expect(products[0].categories).toHaveLength(0)
|
||||
expect(products[0].variants).toHaveLength(1)
|
||||
|
||||
expect(products[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.title,
|
||||
handle: kebabCase(data.title),
|
||||
description: data.description,
|
||||
subtitle: data.subtitle,
|
||||
is_giftcard: data.is_giftcard,
|
||||
discountable: data.discountable,
|
||||
thumbnail: images[0],
|
||||
status: data.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
url: images[0],
|
||||
}),
|
||||
]),
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.options[0].title,
|
||||
values: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.variants[0].options?.[0].value,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
tags: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.tags[0].value,
|
||||
}),
|
||||
]),
|
||||
type: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.type.value,
|
||||
}),
|
||||
variants: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.variants[0].title,
|
||||
sku: data.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: data.variants[0].options?.[0].value,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("softDelete", function () {
|
||||
let module: IProductModuleService
|
||||
let images = ["image-1"]
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEach_()
|
||||
|
||||
MedusaModule.clearInstances()
|
||||
|
||||
module = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(afterEach_)
|
||||
|
||||
it("should soft delete a product and its cascaded relations", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0],
|
||||
})
|
||||
|
||||
const products = await module.create([data])
|
||||
|
||||
await module.softDelete([products[0].id])
|
||||
|
||||
const deletedProducts = await module.list(
|
||||
{ id: products[0].id },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedProducts).toHaveLength(1)
|
||||
expect(deletedProducts[0].deleted_at).not.toBeNull()
|
||||
|
||||
for (const option of deletedProducts[0].options) {
|
||||
expect(option.deleted_at).not.toBeNull()
|
||||
}
|
||||
|
||||
const productOptionsValues = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const optionValue of productOptionsValues) {
|
||||
expect(optionValue.deleted_at).not.toBeNull()
|
||||
}
|
||||
|
||||
for (const variant of deletedProducts[0].variants) {
|
||||
expect(variant.deleted_at).not.toBeNull()
|
||||
}
|
||||
|
||||
const variantsOptions = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const option of variantsOptions) {
|
||||
expect(option.deleted_at).not.toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("restore", function () {
|
||||
let module: IProductModuleService
|
||||
let images = ["image-1"]
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEach_()
|
||||
|
||||
MedusaModule.clearInstances()
|
||||
|
||||
module = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(afterEach_)
|
||||
|
||||
it("should restore a soft deleted product and its cascaded relations", async () => {
|
||||
const data = buildProductAndRelationsData({
|
||||
images,
|
||||
thumbnail: images[0],
|
||||
})
|
||||
|
||||
const products = await module.create([data])
|
||||
|
||||
await module.softDelete([products[0].id])
|
||||
await module.restore([products[0].id])
|
||||
|
||||
const deletedProducts = await module.list(
|
||||
{ id: products[0].id },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deletedProducts).toHaveLength(1)
|
||||
expect(deletedProducts[0].deleted_at).toBeNull()
|
||||
|
||||
for (const option of deletedProducts[0].options) {
|
||||
expect(option.deleted_at).toBeNull()
|
||||
}
|
||||
|
||||
const productOptionsValues = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const optionValue of productOptionsValues) {
|
||||
expect(optionValue.deleted_at).toBeNull()
|
||||
}
|
||||
|
||||
for (const variant of deletedProducts[0].variants) {
|
||||
expect(variant.deleted_at).toBeNull()
|
||||
}
|
||||
|
||||
const variantsOptions = deletedProducts[0].options
|
||||
.map((o) => o.values)
|
||||
.flat()
|
||||
|
||||
for (const option of variantsOptions) {
|
||||
expect(option.deleted_at).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { productCategoriesData } from "../../../__fixtures__/product-category/da
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("ProductCategory Service", () => {
|
||||
describe("Product category Service", () => {
|
||||
let service: ProductCategoryService
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
@@ -47,7 +47,7 @@ describe("ProductCategory Service", () => {
|
||||
const productCategoryResults = await service.list(
|
||||
{},
|
||||
{
|
||||
select: ["id", "parent_category_id"] as any,
|
||||
select: ["id", "parent_category_id"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("ProductCategory Service", () => {
|
||||
include_descendants_tree: true,
|
||||
},
|
||||
{
|
||||
select: ["id", "handle"] as any,
|
||||
select: ["id", "handle"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("ProductCategory Service", () => {
|
||||
is_internal: false,
|
||||
},
|
||||
{
|
||||
select: ["id", "handle"] as any,
|
||||
select: ["id", "handle"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -230,4 +230,334 @@ describe("ProductCategory Service", () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const categoryOneId = "category-1"
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
productCategories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesData
|
||||
)
|
||||
})
|
||||
|
||||
it("should return category for the given id", async () => {
|
||||
const productCategoryResults = await service.retrieve(
|
||||
categoryOneId,
|
||||
)
|
||||
|
||||
expect(productCategoryResults).toEqual(
|
||||
expect.objectContaining({
|
||||
id: categoryOneId
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when category with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('ProductCategory with id: does-not-exist was not found')
|
||||
})
|
||||
|
||||
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('"productCategoryId" must be defined')
|
||||
})
|
||||
|
||||
it("should return category based on config select param", async () => {
|
||||
const productCategoryResults = await service.retrieve(
|
||||
categoryOneId,
|
||||
{
|
||||
select: ["id", "parent_category_id"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryResults).toEqual(
|
||||
expect.objectContaining({
|
||||
id: categoryOneId,
|
||||
parent_category_id: "category-0",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return category based on config relation param", async () => {
|
||||
const productCategoryResults = await service.retrieve(
|
||||
categoryOneId,
|
||||
{
|
||||
select: ["id", "parent_category_id"],
|
||||
relations: ["parent_category"]
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryResults).toEqual(
|
||||
expect.objectContaining({
|
||||
id: categoryOneId,
|
||||
category_children: [
|
||||
expect.objectContaining({
|
||||
id: 'category-1-a',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'category-1-b',
|
||||
})
|
||||
],
|
||||
parent_category: expect.objectContaining({
|
||||
id: "category-0"
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
productCategories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesData
|
||||
)
|
||||
})
|
||||
|
||||
it("should return categories and count based on take and skip", async () => {
|
||||
let results = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(5)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-0",
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
take: 1,
|
||||
skip: 1
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(5)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return all product categories and count", async () => {
|
||||
const productCategoryResults = await service.listAndCount(
|
||||
{},
|
||||
{
|
||||
select: ["id", "parent_category_id"],
|
||||
relations: ["parent_category"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryResults[1]).toEqual(5)
|
||||
expect(productCategoryResults[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-0",
|
||||
parent_category: null
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "category-1",
|
||||
parent_category: expect.objectContaining({
|
||||
id: "category-0",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "category-1-a",
|
||||
parent_category: expect.objectContaining({
|
||||
id: "category-1",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "category-1-b",
|
||||
parent_category: expect.objectContaining({
|
||||
id: "category-1",
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "category-1-b-1",
|
||||
parent_category: expect.objectContaining({
|
||||
id: "category-1-b",
|
||||
}),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should only return categories that are scoped by parent_category_id", async () => {
|
||||
let productCategoryResults = await service.listAndCount(
|
||||
{ parent_category_id: null },
|
||||
{
|
||||
select: ["id"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryResults[1]).toEqual(1)
|
||||
expect(productCategoryResults[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-0",
|
||||
}),
|
||||
])
|
||||
|
||||
productCategoryResults = await service.listAndCount({
|
||||
parent_category_id: "category-0",
|
||||
})
|
||||
|
||||
expect(productCategoryResults[1]).toEqual(1)
|
||||
expect(productCategoryResults[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-1",
|
||||
}),
|
||||
])
|
||||
|
||||
productCategoryResults = await service.listAndCount({
|
||||
parent_category_id: ["category-1-b", "category-0"],
|
||||
})
|
||||
|
||||
expect(productCategoryResults[1]).toEqual(2)
|
||||
expect(productCategoryResults[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "category-1-b-1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should includes descendants when include_descendants_tree is true", async () => {
|
||||
const productCategoryResults = await service.listAndCount(
|
||||
{
|
||||
parent_category_id: null,
|
||||
include_descendants_tree: true,
|
||||
},
|
||||
{
|
||||
select: ["id", "handle"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryResults[1]).toEqual(1)
|
||||
|
||||
const serializedObject = JSON.parse(
|
||||
JSON.stringify(productCategoryResults[0])
|
||||
)
|
||||
|
||||
expect(serializedObject).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-0",
|
||||
handle: "category-0",
|
||||
mpath: "category-0.",
|
||||
parent_category_id: null,
|
||||
parent_category: null,
|
||||
category_children: [
|
||||
expect.objectContaining({
|
||||
id: "category-1",
|
||||
handle: "category-1",
|
||||
mpath: "category-0.category-1.",
|
||||
parent_category_id: "category-0",
|
||||
parent_category: "category-0",
|
||||
category_children: [
|
||||
expect.objectContaining({
|
||||
id: "category-1-a",
|
||||
handle: "category-1-a",
|
||||
mpath: "category-0.category-1.category-1-a.",
|
||||
parent_category_id: "category-1",
|
||||
parent_category: "category-1",
|
||||
category_children: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "category-1-b",
|
||||
handle: "category-1-b",
|
||||
mpath: "category-0.category-1.category-1-b.",
|
||||
parent_category_id: "category-1",
|
||||
parent_category: "category-1",
|
||||
category_children: [
|
||||
expect.objectContaining({
|
||||
id: "category-1-b-1",
|
||||
handle: "category-1-b-1",
|
||||
mpath:
|
||||
"category-0.category-1.category-1-b.category-1-b-1.",
|
||||
parent_category_id: "category-1-b",
|
||||
parent_category: "category-1-b",
|
||||
category_children: [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should filter out children when include_descendants_tree is true", async () => {
|
||||
const productCategoryResults = await service.listAndCount(
|
||||
{
|
||||
parent_category_id: null,
|
||||
include_descendants_tree: true,
|
||||
is_internal: false,
|
||||
},
|
||||
{
|
||||
select: ["id", "handle"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(productCategoryResults[1]).toEqual(1)
|
||||
|
||||
const serializedObject = JSON.parse(
|
||||
JSON.stringify(productCategoryResults[0])
|
||||
)
|
||||
|
||||
expect(serializedObject).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "category-0",
|
||||
handle: "category-0",
|
||||
mpath: "category-0.",
|
||||
parent_category_id: null,
|
||||
parent_category: null,
|
||||
category_children: [
|
||||
expect.objectContaining({
|
||||
id: "category-1",
|
||||
handle: "category-1",
|
||||
mpath: "category-0.category-1.",
|
||||
parent_category_id: "category-0",
|
||||
parent_category: "category-0",
|
||||
category_children: [
|
||||
expect.objectContaining({
|
||||
id: "category-1-a",
|
||||
handle: "category-1-a",
|
||||
mpath: "category-0.category-1.category-1-a.",
|
||||
parent_category_id: "category-1",
|
||||
parent_category: "category-1",
|
||||
category_children: [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createCollections } from "../../../__fixtures__/product"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("Product Service", () => {
|
||||
describe("Product collection Service", () => {
|
||||
let service: ProductCollectionService
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
@@ -59,9 +59,9 @@ describe("Product Service", () => {
|
||||
})
|
||||
|
||||
it("list product collections", async () => {
|
||||
const tagsResults = await service.list()
|
||||
const productCollectionResults = await service.list()
|
||||
|
||||
expect(tagsResults).toEqual([
|
||||
expect(productCollectionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
@@ -82,9 +82,9 @@ describe("Product Service", () => {
|
||||
})
|
||||
|
||||
it("list product collections by id", async () => {
|
||||
const tagsResults = await service.list({ id: data![0].id })
|
||||
const productCollectionResults = await service.list({ id: data![0].id })
|
||||
|
||||
expect(tagsResults).toEqual([
|
||||
expect(productCollectionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
@@ -93,9 +93,9 @@ describe("Product Service", () => {
|
||||
})
|
||||
|
||||
it("list product collections by title matching string", async () => {
|
||||
const tagsResults = await service.list({ title: "col 3 extra" })
|
||||
const productCollectionResults = await service.list({ title: "col 3 extra" })
|
||||
|
||||
expect(tagsResults).toEqual([
|
||||
expect(productCollectionResults).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
@@ -103,4 +103,174 @@ describe("Product Service", () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCount", () => {
|
||||
const data = [
|
||||
{
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
},
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
},
|
||||
{
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
},
|
||||
{
|
||||
id: "test-4",
|
||||
title: "col 4 extra",
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
collectionsData = await createCollections(testManager, data)
|
||||
})
|
||||
|
||||
it("should return all collections and count", async () => {
|
||||
const [productCollectionResults, count] = await service.listAndCount()
|
||||
const serialized = JSON.parse(JSON.stringify(productCollectionResults))
|
||||
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-3",
|
||||
title: "col 3 extra",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "test-4",
|
||||
title: "col 4 extra",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return count and collections based on filter data", async () => {
|
||||
const [productCollectionResults, count] = await service.listAndCount({ id: data![0].id })
|
||||
const serialized = JSON.parse(JSON.stringify(productCollectionResults))
|
||||
|
||||
expect(count).toEqual(1)
|
||||
expect(serialized).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "col 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return count and collections based on config data", async () => {
|
||||
const [productCollectionResults, count] = await service.listAndCount({}, {
|
||||
relations: ['products'],
|
||||
select: ['title'],
|
||||
take: 1,
|
||||
skip: 1,
|
||||
})
|
||||
const serialized = JSON.parse(JSON.stringify(productCollectionResults))
|
||||
|
||||
expect(count).toEqual(4)
|
||||
expect(serialized).toEqual([
|
||||
{
|
||||
id: "test-2",
|
||||
title: "col 2",
|
||||
products: []
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const collectionData = {
|
||||
id: "collection-1",
|
||||
title: "collection 1",
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
await createCollections(testManager, [collectionData])
|
||||
})
|
||||
|
||||
it("should return collection for the given id", async () => {
|
||||
const productCollectionResults = await service.retrieve(
|
||||
collectionData.id,
|
||||
)
|
||||
|
||||
expect(productCollectionResults).toEqual(
|
||||
expect.objectContaining({
|
||||
id: collectionData.id
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when collection with id does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('ProductCollection with id: does-not-exist was not found')
|
||||
})
|
||||
|
||||
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('"productCollectionId" must be defined')
|
||||
})
|
||||
|
||||
it("should return collection based on config select param", async () => {
|
||||
const productCollectionResults = await service.retrieve(
|
||||
collectionData.id,
|
||||
{
|
||||
select: ["id", "title"],
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(productCollectionResults))
|
||||
|
||||
expect(serialized).toEqual(
|
||||
{
|
||||
id: collectionData.id,
|
||||
title: collectionData.title,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("should return collection based on config relation param", async () => {
|
||||
const productCollectionResults = await service.retrieve(
|
||||
collectionData.id,
|
||||
{
|
||||
select: ["id", "title"],
|
||||
relations: ["products"]
|
||||
}
|
||||
)
|
||||
|
||||
const serialized = JSON.parse(JSON.stringify(productCollectionResults))
|
||||
|
||||
expect(serialized).toEqual(
|
||||
{
|
||||
id: collectionData.id,
|
||||
title: collectionData.title,
|
||||
products: []
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { Product, ProductCategory } from "@models"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
import { initialize } from "../../../../src"
|
||||
import { DB_URL, TestDatabase } from "../../../utils"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
|
||||
describe("ProductModuleService product categories", () => {
|
||||
let service: IProductModuleService
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
let productCategoryOne: ProductCategory
|
||||
let productCategoryTwo: ProductCategory
|
||||
let productCategories: ProductCategory[]
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestDatabase.setupDatabase()
|
||||
repositoryManager = await TestDatabase.forkManager()
|
||||
|
||||
service = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
const productCategoriesData = [{
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [productOne],
|
||||
},{
|
||||
id: "test-2",
|
||||
name: "category",
|
||||
products: [productTwo],
|
||||
}]
|
||||
|
||||
productCategories = await createProductCategories(
|
||||
testManager,
|
||||
productCategoriesData
|
||||
)
|
||||
|
||||
productCategoryOne = productCategories[0]
|
||||
productCategoryTwo = productCategories[1]
|
||||
|
||||
await testManager.persistAndFlush([productCategoryOne, productCategoryTwo])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await TestDatabase.clearDatabase()
|
||||
})
|
||||
|
||||
describe("listCategories", () => {
|
||||
it("should return categories queried by ID", async () => {
|
||||
const results = await service.listCategories({
|
||||
id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return categories based on the options and filter parameter", async () => {
|
||||
let results = await service.listCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listCategories({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for categories", async () => {
|
||||
const results = await service.listCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "name", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
})],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountCategories", () => {
|
||||
it("should return categories and count queried by ID", async () => {
|
||||
const results = await service.listAndCountCategories({
|
||||
id: productCategoryOne.id,
|
||||
})
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return categories and count based on the options and filter parameter", async () => {
|
||||
let results = await service.listAndCountCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCountCategories({}, { take: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
|
||||
results = await service.listAndCountCategories({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCategoryTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for categories", async () => {
|
||||
const results = await service.listAndCountCategories(
|
||||
{
|
||||
id: productCategoryOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "name", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
})],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCategory", () => {
|
||||
it("should return the requested category", async () => {
|
||||
const result = await service.retrieveCategory(productCategoryOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieveCategory(
|
||||
productCategoryOne.id,
|
||||
{
|
||||
select: ["id", "name", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
name: "category 1",
|
||||
products: [expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
})],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a category with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveCategory("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("ProductCategory with id: does-not-exist was not found")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { Product, ProductCollection } from "@models"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
import { initialize } from "../../../../src"
|
||||
import { DB_URL, TestDatabase } from "../../../utils"
|
||||
import { createCollections } from "../../../__fixtures__/product"
|
||||
|
||||
describe("ProductModuleService product collections", () => {
|
||||
let service: IProductModuleService
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
let productCollectionOne: ProductCollection
|
||||
let productCollectionTwo: ProductCollection
|
||||
let productCollections: ProductCollection[]
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestDatabase.setupDatabase()
|
||||
repositoryManager = await TestDatabase.forkManager()
|
||||
|
||||
service = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
const productCollectionsData = [{
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [productOne],
|
||||
},{
|
||||
id: "test-2",
|
||||
title: "collection",
|
||||
products: [productTwo],
|
||||
}]
|
||||
|
||||
productCollections = await createCollections(
|
||||
testManager,
|
||||
productCollectionsData
|
||||
)
|
||||
|
||||
productCollectionOne = productCollections[0]
|
||||
productCollectionTwo = productCollections[1]
|
||||
|
||||
await testManager.persistAndFlush([productCollectionOne, productCollectionTwo])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await TestDatabase.clearDatabase()
|
||||
})
|
||||
|
||||
describe("listCollections", () => {
|
||||
it("should return collections queried by ID", async () => {
|
||||
const results = await service.listCollections({
|
||||
id: productCollectionOne.id,
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return collections based on the options and filter parameter", async () => {
|
||||
let results = await service.listCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listCollections({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for collections", async () => {
|
||||
const results = await service.listCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [
|
||||
expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
})
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("listAndCountCollections", () => {
|
||||
it("should return collections and count queried by ID", async () => {
|
||||
const results = await service.listAndCountCollections({
|
||||
id: productCollectionOne.id,
|
||||
})
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return collections and count based on the options and filter parameter", async () => {
|
||||
let results = await service.listAndCountCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCountCollections({}, { take: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
|
||||
results = await service.listAndCountCollections({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: productCollectionTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for collections", async () => {
|
||||
const results = await service.listAndCountCollections(
|
||||
{
|
||||
id: productCollectionOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
})],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCollection", () => {
|
||||
it("should return the requested collection", async () => {
|
||||
const result = await service.retrieveCollection(productCollectionOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieveCollection(
|
||||
productCollectionOne.id,
|
||||
{
|
||||
select: ["id", "title", "products.title"],
|
||||
relations: ["products"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "collection 1",
|
||||
products: [expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
})],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a collection with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveCollection("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("ProductCollection with id: does-not-exist was not found")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { initialize } from "../../../../src"
|
||||
import { DB_URL, TestDatabase } from "../../../utils"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { Product, ProductVariant } from "@models"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
describe("ProductModuleService product variants", () => {
|
||||
let service: IProductModuleService
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
let variantOne: ProductVariant
|
||||
let variantTwo: ProductVariant
|
||||
let productOne: Product
|
||||
let productTwo: Product
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestDatabase.setupDatabase()
|
||||
repositoryManager = await TestDatabase.forkManager()
|
||||
|
||||
service = await initialize({
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA,
|
||||
},
|
||||
})
|
||||
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
variantOne = testManager.create(ProductVariant, {
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
variantTwo = testManager.create(ProductVariant, {
|
||||
id: "test-2",
|
||||
title: "variant",
|
||||
inventory_quantity: 10,
|
||||
product: productTwo,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([variantOne, variantTwo])
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await TestDatabase.clearDatabase()
|
||||
})
|
||||
|
||||
describe("listAndCountVariants", () => {
|
||||
it("should return variants and count queried by ID", async () => {
|
||||
const results = await service.listAndCountVariants({
|
||||
id: variantOne.id,
|
||||
})
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantOne.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return variants and count based on the options and filter parameter", async () => {
|
||||
let results = await service.listAndCountVariants(
|
||||
{
|
||||
id: variantOne.id,
|
||||
},
|
||||
{
|
||||
take: 1,
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantOne.id,
|
||||
}),
|
||||
])
|
||||
|
||||
results = await service.listAndCountVariants({}, { take: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
|
||||
results = await service.listAndCountVariants({}, { take: 1, skip: 1 })
|
||||
|
||||
expect(results[1]).toEqual(2)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: variantTwo.id,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("should return only requested fields and relations for variants", async () => {
|
||||
const results = await service.listAndCountVariants(
|
||||
{
|
||||
id: variantOne.id,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(results[1]).toEqual(1)
|
||||
expect(results[0]).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
product_id: "product-1",
|
||||
// TODO: investigate why this is returning more than the expected results
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveVariant", () => {
|
||||
it("should return the requested variant", async () => {
|
||||
const result = await service.retrieveVariant(variantOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieveVariant(
|
||||
variantOne.id,
|
||||
{
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
title: "variant 1",
|
||||
product_id: "product-1",
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a variant with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieveVariant("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("ProductVariant with id: does-not-exist was not found")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ProductTypes } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("Product Service", () => {
|
||||
describe("Product tag Service", () => {
|
||||
let service: ProductTagService
|
||||
let testManager: SqlEntityManager
|
||||
let repositoryManager: SqlEntityManager
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TestDatabase } from "../../../utils"
|
||||
import { ProductVariantService } from "@services"
|
||||
import { ProductVariantRepository } from "@repositories"
|
||||
import { ProductService, ProductVariantService } from "@services"
|
||||
import { ProductRepository, ProductVariantRepository } from "@repositories"
|
||||
import { Product, ProductTag, ProductVariant } from "@models"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Collection } from "@mikro-orm/core"
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createProductVariants,
|
||||
} from "../../../__fixtures__/product"
|
||||
import { productsData, variantsData } from "../../../__fixtures__/product/data"
|
||||
import { buildProductVariantOnlyData } from "../../../__fixtures__/variant/data/create-variant"
|
||||
|
||||
describe("ProductVariant Service", () => {
|
||||
let service: ProductVariantService
|
||||
@@ -20,6 +21,7 @@ describe("ProductVariant Service", () => {
|
||||
let variantOne: ProductVariant
|
||||
let variantTwo: ProductVariant
|
||||
let productOne: Product
|
||||
const productVariantTestOne = "test-1"
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestDatabase.setupDatabase()
|
||||
@@ -28,8 +30,17 @@ describe("ProductVariant Service", () => {
|
||||
const productVariantRepository = new ProductVariantRepository({
|
||||
manager: repositoryManager,
|
||||
})
|
||||
const productRepository = new ProductRepository({
|
||||
manager: repositoryManager,
|
||||
})
|
||||
|
||||
service = new ProductVariantService({ productVariantRepository })
|
||||
const productService = new ProductService({
|
||||
productRepository,
|
||||
})
|
||||
service = new ProductVariantService({
|
||||
productService,
|
||||
productVariantRepository,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -47,7 +58,7 @@ describe("ProductVariant Service", () => {
|
||||
})
|
||||
|
||||
variantOne = testManager.create(ProductVariant, {
|
||||
id: "test-1",
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
@@ -95,7 +106,7 @@ describe("ProductVariant Service", () => {
|
||||
it("passing populate, scopes the results of the response", async () => {
|
||||
const results = await service.list(
|
||||
{
|
||||
id: "test-1",
|
||||
id: productVariantTestOne,
|
||||
},
|
||||
{
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
@@ -105,7 +116,7 @@ describe("ProductVariant Service", () => {
|
||||
|
||||
expect(results).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
@@ -118,7 +129,7 @@ describe("ProductVariant Service", () => {
|
||||
|
||||
expect(JSON.parse(JSON.stringify(results))).toEqual([
|
||||
{
|
||||
id: "test-1",
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
product_id: "product-1",
|
||||
product: {
|
||||
@@ -175,11 +186,148 @@ describe("ProductVariant Service", () => {
|
||||
|
||||
expect(JSON.parse(JSON.stringify(variants))).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "test-1",
|
||||
id: productVariantTestOne,
|
||||
title: "variant title",
|
||||
sku: "sku 1",
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let products: Product[]
|
||||
let productOptions!: ProductOption[]
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
products = (await createProductAndTags(
|
||||
testManager,
|
||||
productsData
|
||||
)) as Product[]
|
||||
|
||||
productOptions = await createOptions(testManager, [
|
||||
{
|
||||
id: "test-option-1",
|
||||
title: "size",
|
||||
product: products[0],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should create a variant", async () => {
|
||||
const data = buildProductVariantOnlyData({
|
||||
options: [
|
||||
{
|
||||
option: productOptions[0],
|
||||
value: "XS",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const variants = await service.create(products[0].id, [data])
|
||||
|
||||
expect(variants).toHaveLength(1)
|
||||
expect(variants[0].options).toHaveLength(1)
|
||||
|
||||
expect(JSON.parse(JSON.stringify(variants[0]))).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.title,
|
||||
sku: data.sku,
|
||||
inventory_quantity: 100,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
variant_rank: 0,
|
||||
product: expect.objectContaining({
|
||||
id: products[0].id,
|
||||
}),
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
value: data.options![0].value,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
productOne = testManager.create(Product, {
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
status: ProductTypes.ProductStatus.PUBLISHED,
|
||||
})
|
||||
|
||||
variantOne = testManager.create(ProductVariant, {
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
inventory_quantity: 10,
|
||||
product: productOne,
|
||||
})
|
||||
|
||||
await testManager.persistAndFlush([variantOne])
|
||||
})
|
||||
|
||||
it("should return the requested variant", async () => {
|
||||
const result = await service.retrieve(variantOne.id)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should return requested attributes when requested through config", async () => {
|
||||
const result = await service.retrieve(
|
||||
variantOne.id,
|
||||
{
|
||||
select: ["id", "title", "product.title"] as any,
|
||||
relations: ["product"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productVariantTestOne,
|
||||
title: "variant 1",
|
||||
product_id: "product-1",
|
||||
product: expect.objectContaining({
|
||||
id: "product-1",
|
||||
title: "product 1",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a variant with ID does not exist", async () => {
|
||||
let error
|
||||
|
||||
try {
|
||||
await service.retrieve("does-not-exist")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual("ProductVariant with id: does-not-exist was not found")
|
||||
})
|
||||
|
||||
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('"productVariantId" must be defined')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { TestDatabase } from "../../../utils"
|
||||
import {
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductVariantService,
|
||||
} from "@services"
|
||||
import { ProductService } from "@services"
|
||||
import { ProductRepository } from "@repositories"
|
||||
import { Product, ProductCategory, ProductVariant } from "@models"
|
||||
import { Image, Product, ProductCategory, ProductVariant } from "@models"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductDTO } from "@medusajs/types"
|
||||
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import {
|
||||
assignCategoriesToProduct,
|
||||
createImages,
|
||||
createProductAndTags,
|
||||
createProductVariants,
|
||||
} from "../../../__fixtures__/product"
|
||||
@@ -20,13 +17,8 @@ import {
|
||||
productsData,
|
||||
variantsData,
|
||||
} from "../../../__fixtures__/product/data"
|
||||
|
||||
const productVariantService = {
|
||||
list: jest.fn(),
|
||||
} as unknown as ProductVariantService
|
||||
const productTagService = {
|
||||
list: jest.fn(),
|
||||
} as unknown as ProductTagService
|
||||
import { buildProductOnlyData } from "../../../__fixtures__/product/data/create-product"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -48,8 +40,6 @@ describe("Product Service", () => {
|
||||
|
||||
service = new ProductService({
|
||||
productRepository,
|
||||
productVariantService,
|
||||
productTagService,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,7 +47,74 @@ describe("Product Service", () => {
|
||||
await TestDatabase.clearDatabase()
|
||||
})
|
||||
|
||||
describe("create", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should create a product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
|
||||
expect(products).toHaveLength(1)
|
||||
expect(JSON.parse(JSON.stringify(products[0]))).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: data.title,
|
||||
handle: kebabCase(data.title),
|
||||
description: data.description,
|
||||
subtitle: data.subtitle,
|
||||
is_giftcard: data.is_giftcard,
|
||||
discountable: data.discountable,
|
||||
thumbnail: images[0].url,
|
||||
status: data.status,
|
||||
images: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: images[0].id,
|
||||
url: images[0].url,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
describe("soft deleted", function () {
|
||||
let deletedProduct
|
||||
let product
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
const products = await createProductAndTags(testManager, productsData)
|
||||
|
||||
product = products[1]
|
||||
deletedProduct = await service.softDelete([products[0].id])
|
||||
})
|
||||
|
||||
it("should list all products that are not deleted", async () => {
|
||||
const products = await service.list()
|
||||
|
||||
expect(products).toHaveLength(1)
|
||||
expect(products[0].id).toEqual(product.id)
|
||||
})
|
||||
|
||||
it("should list all products including the deleted", async () => {
|
||||
const products = await service.list({}, { withDeleted: true })
|
||||
|
||||
expect(products).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("relation: tags", () => {
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
@@ -65,7 +122,7 @@ describe("Product Service", () => {
|
||||
products = await createProductAndTags(testManager, productsData)
|
||||
})
|
||||
|
||||
it("filter by id and including relations", async () => {
|
||||
it("should filter by id and including relations", async () => {
|
||||
const productsResult = await service.list(
|
||||
{
|
||||
id: products[0].id,
|
||||
@@ -95,7 +152,7 @@ describe("Product Service", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("filter by id and without relations", async () => {
|
||||
it("should filter by id and without relations", async () => {
|
||||
const productsResult = await service.list({
|
||||
id: products[0].id,
|
||||
})
|
||||
@@ -137,7 +194,7 @@ describe("Product Service", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("filter by categories relation and scope fields", async () => {
|
||||
it("should filter by categories relation and scope fields", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
id: workingProduct.id,
|
||||
@@ -187,7 +244,7 @@ describe("Product Service", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("returns empty array when querying for a category that doesnt exist", async () => {
|
||||
it("should returns empty array when querying for a category that doesnt exist", async () => {
|
||||
const products = await service.list(
|
||||
{
|
||||
id: workingProduct.id,
|
||||
@@ -215,7 +272,7 @@ describe("Product Service", () => {
|
||||
variants = await createProductVariants(testManager, variantsData)
|
||||
})
|
||||
|
||||
it("filter by id and including relations", async () => {
|
||||
it("should filter by id and including relations", async () => {
|
||||
const productsResult = await service.list(
|
||||
{
|
||||
id: products[0].id,
|
||||
@@ -254,4 +311,52 @@ describe("Product Service", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("softDelete", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should soft delete a product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
const deleteProducts = await service.softDelete(products.map((p) => p.id))
|
||||
|
||||
expect(deleteProducts).toHaveLength(1)
|
||||
expect(deleteProducts[0].deleted_at).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("restore", function () {
|
||||
let images: Image[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
testManager = await TestDatabase.forkManager()
|
||||
|
||||
images = await createImages(testManager, ["image-1"])
|
||||
})
|
||||
|
||||
it("should restore a soft deleted product", async () => {
|
||||
const data = buildProductOnlyData({
|
||||
images,
|
||||
thumbnail: images[0].url,
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
const product = products[0]
|
||||
await service.softDelete([product.id])
|
||||
const restoreProducts = await service.restore([product.id])
|
||||
|
||||
expect(restoreProducts).toHaveLength(1)
|
||||
expect(restoreProducts[0].deleted_at).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const { dropDatabase } = require("pg-god")
|
||||
|
||||
const DB_HOST = process.env.DB_HOST
|
||||
const DB_USERNAME = process.env.DB_USERNAME
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? "postgres"
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
|
||||
const DB_NAME = process.env.DB_TEMP_NAME
|
||||
|
||||
const pgGodCredentials = {
|
||||
@@ -16,7 +16,7 @@ afterAll(async () => {
|
||||
await dropDatabase({ databaseName: DB_NAME }, pgGodCredentials)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`This might fail if it is run during the unit tests since there is no database to drop. Otherwise, please check what is the issue. ${e}`
|
||||
`This might fail if it is run during the unit tests since there is no database to drop. Otherwise, please check what is the issue. ${e.message}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@mikro-orm/cli": "5.7.12",
|
||||
"@mikro-orm/migrations": "5.7.12",
|
||||
"cross-env": "^5.2.1",
|
||||
"faker": "^6.6.6",
|
||||
"jest": "^25.5.4",
|
||||
"medusa-test-utils": "^1.1.40",
|
||||
"pg-god": "^1.0.12",
|
||||
|
||||
@@ -5,18 +5,14 @@ import {
|
||||
MODULE_PACKAGE_NAMES,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
import {
|
||||
InitializeModuleInjectableDependencies,
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { InitializeModuleInjectableDependencies } from "../types"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration,
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<IProductModuleService> => {
|
||||
|
||||
@@ -6,24 +6,21 @@ import {
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { MedusaError, ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
|
||||
import * as ProductModels from "@models"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { createConnection } from "../utils"
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
|
||||
export default async (
|
||||
{
|
||||
options,
|
||||
container,
|
||||
}: LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void> => {
|
||||
@@ -35,11 +32,11 @@ export default async (
|
||||
}
|
||||
|
||||
const customManager = (
|
||||
options as ProductServiceInitializeCustomDataLayerOptions
|
||||
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
)?.manager
|
||||
|
||||
if (!customManager) {
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
await loadDefault({ database: dbData, container })
|
||||
} else {
|
||||
container.register({
|
||||
|
||||
@@ -3,36 +3,39 @@ import { LoaderOptions } from "@medusajs/modules-sdk"
|
||||
import { asClass } from "awilix"
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductImageService,
|
||||
ProductModuleService,
|
||||
ProductOptionService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
ProductVariantService,
|
||||
ProductCollectionService,
|
||||
} from "@services"
|
||||
import * as DefaultRepositories from "@repositories"
|
||||
import {
|
||||
BaseRepository,
|
||||
ProductCategoryRepository,
|
||||
ProductCollectionRepository,
|
||||
ProductImageRepository,
|
||||
ProductOptionRepository,
|
||||
ProductRepository,
|
||||
ProductTagRepository,
|
||||
ProductTypeRepository,
|
||||
ProductVariantRepository,
|
||||
} from "@repositories"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { Constructor, DAL } from "@medusajs/types"
|
||||
import { Constructor, DAL, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { lowerCaseFirst } from "@medusajs/utils"
|
||||
|
||||
export default async ({
|
||||
container,
|
||||
options,
|
||||
}: LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>): Promise<void> => {
|
||||
const customRepositories = (
|
||||
options as ProductServiceInitializeCustomDataLayerOptions
|
||||
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
)?.repositories
|
||||
|
||||
container.register({
|
||||
@@ -42,6 +45,9 @@ export default async ({
|
||||
productVariantService: asClass(ProductVariantService).singleton(),
|
||||
productTagService: asClass(ProductTagService).singleton(),
|
||||
productCollectionService: asClass(ProductCollectionService).singleton(),
|
||||
productImageService: asClass(ProductImageService).singleton(),
|
||||
productTypeService: asClass(ProductTypeService).singleton(),
|
||||
productOptionService: asClass(ProductOptionService).singleton(),
|
||||
})
|
||||
|
||||
if (customRepositories) {
|
||||
@@ -53,13 +59,17 @@ export default async ({
|
||||
|
||||
function loadDefaultRepositories({ container }) {
|
||||
container.register({
|
||||
productRepository: asClass(ProductRepository).singleton(),
|
||||
productVariantRepository: asClass(ProductVariantRepository).singleton(),
|
||||
productTagRepository: asClass(ProductTagRepository).singleton(),
|
||||
baseRepository: asClass(BaseRepository).singleton(),
|
||||
productImageRepository: asClass(ProductImageRepository).singleton(),
|
||||
productCategoryRepository: asClass(ProductCategoryRepository).singleton(),
|
||||
productCollectionRepository: asClass(
|
||||
ProductCollectionRepository
|
||||
).singleton(),
|
||||
productRepository: asClass(ProductRepository).singleton(),
|
||||
productTagRepository: asClass(ProductTagRepository).singleton(),
|
||||
productTypeRepository: asClass(ProductTypeRepository).singleton(),
|
||||
productOptionRepository: asClass(ProductOptionRepository).singleton(),
|
||||
productVariantRepository: asClass(ProductVariantRepository).singleton(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,15 @@
|
||||
"name": "product_collection",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_collection_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_product_collection_handle_unique",
|
||||
"columnNames": [
|
||||
@@ -234,6 +243,80 @@
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "image",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_image_url",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_image_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "image_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -277,6 +360,15 @@
|
||||
"name": "product_tag",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_tag_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "product_tag_pkey",
|
||||
"columnNames": [
|
||||
@@ -333,6 +425,15 @@
|
||||
"name": "product_type",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_type_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "product_type_pkey",
|
||||
"columnNames": [
|
||||
@@ -588,6 +689,15 @@
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_product_handle_unique",
|
||||
"columnNames": [
|
||||
@@ -698,6 +808,15 @@
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "product_option_pkey",
|
||||
"columnNames": [
|
||||
@@ -789,6 +908,71 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"product_image_id": {
|
||||
"name": "product_image_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
}
|
||||
},
|
||||
"name": "product_images",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "product_images_pkey",
|
||||
"columnNames": [
|
||||
"product_id",
|
||||
"product_image_id"
|
||||
],
|
||||
"composite": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"product_images_product_id_foreign": {
|
||||
"constraintName": "product_images_product_id_foreign",
|
||||
"columnNames": [
|
||||
"product_id"
|
||||
],
|
||||
"localTableName": "public.product_images",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.product",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"product_images_product_image_id_foreign": {
|
||||
"constraintName": "product_images_product_image_id_foreign",
|
||||
"columnNames": [
|
||||
"product_image_id"
|
||||
],
|
||||
"localTableName": "public.product_images",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.image",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
@@ -917,6 +1101,7 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "100",
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"allow_backorder": {
|
||||
@@ -1027,6 +1212,7 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"default": "0",
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"created_at": {
|
||||
@@ -1072,12 +1258,21 @@
|
||||
"name": "product_variant",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_variant_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"product_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_variant_product_id_index",
|
||||
"keyName": "IDX_product_variant_product_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
@@ -1210,7 +1405,25 @@
|
||||
"option_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_value_product_option",
|
||||
"keyName": "IDX_product_option_value_option_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"variant_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_value_variant_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_value_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20230609132805 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "product_category_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_category_path" on "product_category" ("mpath");');
|
||||
this.addSql('alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");');
|
||||
|
||||
this.addSql('create table "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));');
|
||||
this.addSql('alter table "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");');
|
||||
|
||||
this.addSql('create table "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
||||
|
||||
this.addSql('create table "product_type" ("id" text not null, "value" text not null, "metadata" json null, "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));');
|
||||
|
||||
this.addSql('create table "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_type_id" on "product" ("type_id");');
|
||||
this.addSql('alter table "product" add constraint "IDX_product_handle_unique" unique ("handle");');
|
||||
|
||||
this.addSql('create table "product_option" ("id" text not null, "title" text not null, "product_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_option_product_id" on "product_option" ("product_id");');
|
||||
|
||||
this.addSql('create table "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));');
|
||||
|
||||
this.addSql('create table "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));');
|
||||
|
||||
this.addSql('create table "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "product_id" text not null, constraint "product_variant_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_variant_product_id_index" on "product_variant" ("product_id");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");');
|
||||
|
||||
this.addSql('create table "product_option_value" ("id" text not null, "value" text not null, "option_id" text not null, "variant_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_option_value_product_option" on "product_option_value" ("option_id");');
|
||||
|
||||
this.addSql('alter table "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;');
|
||||
|
||||
this.addSql('alter table "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;');
|
||||
this.addSql('alter table "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;');
|
||||
|
||||
this.addSql('alter table "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade;');
|
||||
|
||||
this.addSql('alter table "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade;');
|
||||
this.addSql('alter table "product_option_value" add constraint "product_option_value_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20230710091208 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'create table "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "product_category_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_category_path" on "product_category" ("mpath");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql('create index "IDX_product_image_url" on "image" ("url");')
|
||||
this.addSql(
|
||||
'create index "IDX_product_image_deleted_at" on "image" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_type" ("id" text not null, "value" text not null, "metadata" json null, "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_type_deleted_at" on "product_type" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql('create index "IDX_product_type_id" on "product" ("type_id");')
|
||||
this.addSql(
|
||||
'create index "IDX_product_deleted_at" on "product" ("deleted_at");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product" add constraint "IDX_product_handle_unique" unique ("handle");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_option" ("id" text not null, "title" text not null, "product_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_product_id" on "product_option" ("product_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_deleted_at" on "product_option" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_images" ("product_id" text not null, "product_image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "product_image_id"));'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "product_id" text not null, constraint "product_variant_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_variant_product_id" on "product_variant" ("product_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_option_value" ("id" text not null, "value" text not null, "option_id" text not null, "variant_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_value_option_id" on "product_option_value" ("option_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_value_variant_id" on "product_option_value" ("variant_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_images" add constraint "product_images_product_image_id_foreign" foreign key ("product_image_id") references "image" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_option_value" add constraint "product_option_value_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,5 @@ export { default as ProductCollection } from "./product-collection"
|
||||
export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as Image } from "./product-image"
|
||||
|
||||
@@ -21,7 +21,7 @@ class ProductCategory {
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
name: string
|
||||
name?: string
|
||||
|
||||
@Property({ columnType: "text", default: "", nullable: false })
|
||||
description?: string
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId, kebabCase } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "product_collection" })
|
||||
@SoftDeletable()
|
||||
class ProductCollection {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,13 +32,17 @@ class ProductCollection {
|
||||
name: "IDX_product_collection_handle_unique",
|
||||
properties: ["handle"],
|
||||
})
|
||||
handle: string
|
||||
handle?: string
|
||||
|
||||
@OneToMany(() => Product, (product) => product.collection)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_collection_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "image" })
|
||||
@SoftDeletable()
|
||||
class ProductImage {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Index({ name: "IDX_product_image_url" })
|
||||
@Property({ columnType: "text" })
|
||||
url: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_image_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.images)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductImage
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
@@ -9,28 +11,53 @@ import { generateEntityId } from "@medusajs/utils"
|
||||
|
||||
import ProductOption from "./product-option"
|
||||
import { ProductVariant } from "./index"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "option_id"
|
||||
| "variant_id"
|
||||
type OptionalRelations = "product" | "option" | "variant"
|
||||
|
||||
@Entity({ tableName: "product_option_value" })
|
||||
@SoftDeletable()
|
||||
class ProductOptionValue {
|
||||
[OptionalProps]?: OptionalFields | OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ persist: false })
|
||||
option_id!: string
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
index: "IDX_product_option_value_product_option",
|
||||
index: "IDX_product_option_value_option_id",
|
||||
fieldName: "option_id",
|
||||
})
|
||||
option: ProductOption
|
||||
|
||||
@ManyToOne(() => ProductVariant, { onDelete: "cascade" })
|
||||
@Property({ persist: false })
|
||||
variant_id!: string
|
||||
|
||||
@ManyToOne(() => ProductVariant, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_option_value_variant_id",
|
||||
fieldName: "variant_id",
|
||||
})
|
||||
variant: ProductVariant
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_option_value_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
|
||||
@@ -3,17 +3,26 @@ import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "./index"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "values" | "product"
|
||||
type OptionalFields = "product_id"
|
||||
|
||||
@Entity({ tableName: "product_option" })
|
||||
@SoftDeletable()
|
||||
class ProductOption {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,23 +30,25 @@ class ProductOption {
|
||||
title: string
|
||||
|
||||
@Property({ persist: false })
|
||||
product_id!: number
|
||||
product_id!: string
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
index: "IDX_product_option_product_id",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (value) => value.option, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
cascade: [Cascade.REMOVE, "soft-remove" as any],
|
||||
})
|
||||
values = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_option_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
|
||||
@@ -2,16 +2,24 @@ import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "product_tag" })
|
||||
@SoftDeletable()
|
||||
class ProductTag {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,8 +29,9 @@ class ProductTag {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_tag_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.tags)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { BeforeCreate, Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
@Entity({ tableName: "product_type" })
|
||||
@SoftDeletable()
|
||||
class ProductType {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
@@ -13,8 +21,9 @@ class ProductType {
|
||||
@Property({ columnType: "json", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_type_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
@@ -13,18 +14,18 @@ import {
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "product"
|
||||
| "product_id"
|
||||
|
||||
@Entity({ tableName: "product_variant" })
|
||||
@SoftDeletable()
|
||||
class ProductVariant {
|
||||
[OptionalProps]?: OptionalFields
|
||||
|
||||
@@ -65,14 +66,14 @@ class ProductVariant {
|
||||
// Note: Upon serialization, this turns to a string. This is on purpose, because you would loose
|
||||
// precision if you cast numeric to JS number, as JS number is a float.
|
||||
// Ref: https://github.com/mikro-orm/mikro-orm/issues/2295
|
||||
@Property({ columnType: "numeric" })
|
||||
inventory_quantity: number
|
||||
@Property({ columnType: "numeric", default: 100 })
|
||||
inventory_quantity?: number = 100
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
allow_backorder: boolean
|
||||
allow_backorder?: boolean = false
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
manage_inventory: boolean
|
||||
manage_inventory?: boolean = true
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
@@ -101,7 +102,7 @@ class ProductVariant {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
@Property({ columnType: "numeric", nullable: true, default: 0 })
|
||||
variant_rank?: number | null
|
||||
|
||||
@Property({ persist: false })
|
||||
@@ -117,18 +118,19 @@ class ProductVariant {
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_variant_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_variant_product_id_index",
|
||||
index: "IDX_product_variant_product_id",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product!: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, {
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE],
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any],
|
||||
})
|
||||
options = new Collection<ProductOptionValue>(this)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
@@ -20,16 +21,20 @@ import ProductOption from "./product-option"
|
||||
import ProductTag from "./product-tag"
|
||||
import ProductType from "./product-type"
|
||||
import ProductVariant from "./product-variant"
|
||||
import ProductImage from "./product-image"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "collection" | "type"
|
||||
type OptionalFields =
|
||||
| "collection_id"
|
||||
| "type_id"
|
||||
| "is_giftcard"
|
||||
| "discountable"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
|
||||
@Entity({ tableName: "product" })
|
||||
@SoftDeletable()
|
||||
class Product {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@@ -58,16 +63,17 @@ class Product {
|
||||
@Enum(() => ProductTypes.ProductStatus)
|
||||
status!: ProductTypes.ProductStatus
|
||||
|
||||
// TODO: add images model
|
||||
// images: Image[]
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@OneToMany(() => ProductOption, (o) => o.product)
|
||||
@OneToMany(() => ProductOption, (o) => o.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product)
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@@ -94,12 +100,22 @@ class Product {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@ManyToOne(() => ProductCollection, { nullable: true })
|
||||
@Property({ persist: false })
|
||||
collection_id!: string
|
||||
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
nullable: true,
|
||||
fieldName: "collection_id",
|
||||
})
|
||||
collection!: ProductCollection
|
||||
|
||||
@Property({ persist: false })
|
||||
type_id!: string
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
nullable: true,
|
||||
index: "IDX_product_type_id",
|
||||
fieldName: "type_id",
|
||||
})
|
||||
type!: ProductType
|
||||
|
||||
@@ -107,12 +123,22 @@ class Product {
|
||||
owner: true,
|
||||
pivotTable: "product_tags",
|
||||
index: "IDX_product_tag_id",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductImage, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_images",
|
||||
index: "IDX_product_image_id",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
images = new Collection<ProductImage>(this)
|
||||
|
||||
@ManyToMany(() => ProductCategory, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_category_product",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
categories = new Collection<ProductCategory>(this)
|
||||
|
||||
@@ -132,8 +158,9 @@ class Product {
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ProductModuleService } from "@services"
|
||||
import loadContainer from "./loaders/container"
|
||||
import loadConnection from "./loaders/connection"
|
||||
import * as ProductModels from "@models"
|
||||
import { revertMigration, runMigrations } from "./scripts"
|
||||
|
||||
const service = ProductModuleService
|
||||
const loaders = [loadContainer, loadConnection] as any
|
||||
@@ -13,6 +12,4 @@ export const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
models,
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
buildQuery,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
import { serialize } from "@mikro-orm/core"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
// TODO: Should we create a mikro orm specific package for this and the soft deletable decorator util?
|
||||
|
||||
async function transactionWrapper(
|
||||
this: any,
|
||||
task: (transactionManager: unknown) => Promise<any>,
|
||||
{
|
||||
transaction,
|
||||
isolationLevel,
|
||||
enableNestedTransactions = false,
|
||||
}: {
|
||||
isolationLevel?: string
|
||||
transaction?: unknown
|
||||
enableNestedTransactions?: boolean
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
// Reuse the same transaction if it is already provided and nested transactions are disabled
|
||||
if (!enableNestedTransactions && transaction) {
|
||||
return await task(transaction)
|
||||
}
|
||||
|
||||
const forkedManager = this.manager_.fork()
|
||||
|
||||
const options = {}
|
||||
if (isolationLevel) {
|
||||
Object.assign(options, { isolationLevel })
|
||||
}
|
||||
|
||||
if (transaction) {
|
||||
Object.assign(options, { ctx: transaction })
|
||||
await forkedManager.begin(options)
|
||||
} else {
|
||||
await forkedManager.begin(options)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await task(forkedManager)
|
||||
await forkedManager.commit()
|
||||
return result
|
||||
} catch (e) {
|
||||
await forkedManager.rollback()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const updateDeletedAtRecursively = async <T extends object = any>(
|
||||
manager: SqlEntityManager,
|
||||
entities: T[],
|
||||
value: Date | null
|
||||
) => {
|
||||
for await (const entity of entities) {
|
||||
if (!("deleted_at" in entity)) continue
|
||||
;(entity as any).deleted_at = value
|
||||
|
||||
const relations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entities[0].constructor.name).relations
|
||||
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
const relationEntities = (await entity[relation.name].init()).getItems({
|
||||
filters: {
|
||||
[DAL.SoftDeletableFilterKey]: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await updateDeletedAtRecursively(manager, relationEntities, value)
|
||||
}
|
||||
|
||||
await manager.persist(entities)
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = <
|
||||
T extends object | object[],
|
||||
TResult extends object | object[]
|
||||
>(
|
||||
data: T,
|
||||
options?: any
|
||||
): Promise<TResult> => {
|
||||
options ??= {}
|
||||
const result = serialize(data, options)
|
||||
return Array.isArray(data) ? result : result[0]
|
||||
}
|
||||
|
||||
export abstract class AbstractBaseRepository<T = any>
|
||||
implements DAL.RepositoryService<T>
|
||||
{
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
protected constructor({ manager }) {
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async transaction(
|
||||
task: (transactionManager: unknown) => Promise<any>,
|
||||
{
|
||||
transaction,
|
||||
isolationLevel,
|
||||
enableNestedTransactions = false,
|
||||
}: {
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
transaction?: unknown
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
return await transactionWrapper.apply(this, arguments)
|
||||
}
|
||||
|
||||
serialize<
|
||||
TData extends object | object[] = object[],
|
||||
TResult extends object | object[] = object[]
|
||||
>(data: TData, options?: any): Promise<TResult> {
|
||||
return serializer<TData, TResult>(data, options)
|
||||
}
|
||||
|
||||
abstract find(options?: DAL.FindOptions<T>, context?: Context)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
abstract create(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
@InjectTransactionManager()
|
||||
async softDelete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<T[]> {
|
||||
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
||||
|
||||
const date = new Date()
|
||||
await updateDeletedAtRecursively(
|
||||
manager as SqlEntityManager,
|
||||
entities,
|
||||
date
|
||||
)
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async restore(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context<SqlEntityManager> = {}
|
||||
): Promise<T[]> {
|
||||
const query = buildQuery(
|
||||
{ id: { $in: ids } },
|
||||
{
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
const entities = await this.find(query)
|
||||
|
||||
await updateDeletedAtRecursively(
|
||||
manager as SqlEntityManager,
|
||||
entities,
|
||||
null
|
||||
)
|
||||
|
||||
return entities
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractTreeRepositoryBase<T = any>
|
||||
extends AbstractBaseRepository<T>
|
||||
implements DAL.TreeRepositoryService<T>
|
||||
{
|
||||
protected constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
abstract find(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used internally in order to be able to wrap in transaction from a
|
||||
* non identified repository
|
||||
*/
|
||||
export class BaseRepository extends AbstractBaseRepository {
|
||||
constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
serialize<
|
||||
TData extends object | object[] = object[],
|
||||
TResult extends object | object[] = object[]
|
||||
>(data: TData, options?: any): Promise<TResult> {
|
||||
return serializer<TData, TResult>(data, options)
|
||||
}
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(ids: string[], context?: Context): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
find(options?: DAL.FindOptions, context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions,
|
||||
context?: Context
|
||||
): Promise<[any[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
export { BaseRepository } from "./base"
|
||||
export { ProductRepository } from "./product"
|
||||
export { ProductTagRepository } from "./product-tag"
|
||||
export { ProductVariantRepository } from "./product-variant"
|
||||
export { ProductCollectionRepository } from "./product-collection"
|
||||
export { ProductCategoryRepository } from "./product-category"
|
||||
export { ProductImageRepository } from "./product-image"
|
||||
export { ProductTypeRepository } from "./product-type"
|
||||
export { ProductOptionRepository } from "./product-option"
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { ProductCategory } from "@models"
|
||||
import { DAL, ProductCategoryTransformOptions } from "@medusajs/types"
|
||||
import { Product, ProductCategory } from "@models"
|
||||
import { Context, DAL, ProductCategoryTransformOptions } from "@medusajs/types"
|
||||
import groupBy from "lodash/groupBy"
|
||||
import { AbstractTreeRepositoryBase } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductCategoryRepository
|
||||
implements DAL.RepositoryService<ProductCategory>
|
||||
{
|
||||
export class ProductCategoryRepository extends AbstractTreeRepositoryBase<ProductCategory> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
const { includeDescendantsTree } = transformOptions
|
||||
|
||||
findOptions_.options ??= {}
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
@@ -39,19 +40,11 @@ export class ProductCategoryRepository
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
const productCategories = await this.manager_.find(
|
||||
const productCategories = await manager.find(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
@@ -69,8 +62,12 @@ export class ProductCategoryRepository
|
||||
|
||||
async buildProductCategoriesWithDescendants(
|
||||
productCategories: ProductCategory[],
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
for (let productCategory of productCategories) {
|
||||
const whereOptions = {
|
||||
...findOptions.where,
|
||||
@@ -78,9 +75,11 @@ export class ProductCategoryRepository
|
||||
$like: `${productCategory.mpath}%`,
|
||||
},
|
||||
}
|
||||
delete whereOptions.parent_category_id
|
||||
|
||||
const descendantsForCategory = await this.manager_.find(
|
||||
delete whereOptions.parent_category_id
|
||||
delete whereOptions.id
|
||||
|
||||
const descendantsForCategory = await manager.find(
|
||||
ProductCategory,
|
||||
whereOptions as MikroFilterQuery<ProductCategory>,
|
||||
findOptions.options as MikroOptions<ProductCategory>
|
||||
@@ -111,30 +110,64 @@ export class ProductCategoryRepository
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<[ProductCategory[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
const { includeDescendantsTree } = transformOptions
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
if (includeDescendantsTree) {
|
||||
fields.indexOf("mpath") === -1 && fields.push("mpath")
|
||||
fields.indexOf("parent_category_id") === -1 &&
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await this.manager_.findAndCount(
|
||||
const [productCategories, count] = await manager.findAndCount(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
|
||||
if (!includeDescendantsTree) {
|
||||
return [productCategories, count]
|
||||
}
|
||||
|
||||
return [
|
||||
await this.buildProductCategoriesWithDescendants(
|
||||
productCategories,
|
||||
findOptions_
|
||||
),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
async create(
|
||||
data: unknown[],
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,84 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductCollection } from "@models"
|
||||
import { Product, ProductCollection } from "@models"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductCollectionRepository implements DAL.RepositoryService {
|
||||
export class ProductCollectionRepository extends AbstractBaseRepository<ProductCollection> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find<T = ProductCollection>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<T[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductCollection> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductCollection[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.find(
|
||||
return await manager.find(
|
||||
ProductCollection,
|
||||
findOptions_.where as MikroFilterQuery<ProductCollection>,
|
||||
findOptions_.options as MikroOptions<ProductCollection>
|
||||
)) as unknown as T[]
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount<T = ProductCollection>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<[T[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductCollection> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductCollection[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.findAndCount(
|
||||
return await manager.findAndCount(
|
||||
ProductCollection,
|
||||
findOptions_.where as MikroFilterQuery<ProductCollection>,
|
||||
findOptions_.options as MikroOptions<ProductCollection>
|
||||
)) as unknown as [T[], number]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductCollection[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { Image, Product } from "@models"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductImageRepository extends AbstractBaseRepository<Image> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<Image> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<Image[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
Image,
|
||||
findOptions_.where as MikroFilterQuery<Image>,
|
||||
findOptions_.options as MikroOptions<Image>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<Image> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[Image[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
Image,
|
||||
findOptions_.where as MikroFilterQuery<Image>,
|
||||
findOptions_.options as MikroOptions<Image>
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async upsert(
|
||||
urls: string[],
|
||||
@MedusaContext()
|
||||
context: Context = {}
|
||||
): Promise<Image[]> {
|
||||
const { transactionManager: manager } = context
|
||||
|
||||
const existingImages = await this.find(
|
||||
{
|
||||
where: {
|
||||
url: {
|
||||
$in: urls,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingImagesMap = new Map(
|
||||
existingImages.map<[string, Image]>((img) => [img.url, img])
|
||||
)
|
||||
|
||||
const upsertedImgs: Image[] = []
|
||||
const imageToCreate: Image[] = []
|
||||
|
||||
urls.forEach((url) => {
|
||||
const aImg = existingImagesMap.get(url)
|
||||
if (aImg) {
|
||||
upsertedImgs.push(aImg)
|
||||
} else {
|
||||
const newImg = (manager as SqlEntityManager).create(Image, { url })
|
||||
imageToCreate.push(newImg)
|
||||
}
|
||||
})
|
||||
|
||||
if (imageToCreate.length) {
|
||||
await (manager as SqlEntityManager).persist(imageToCreate)
|
||||
upsertedImgs.push(...imageToCreate)
|
||||
}
|
||||
|
||||
return upsertedImgs
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<Image[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product, ProductOption } from "@models"
|
||||
import { Context, DAL, ProductTypes } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductOptionRepository extends AbstractBaseRepository<ProductOption> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductOption> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductOption[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
ProductOption,
|
||||
findOptions_.where as MikroFilterQuery<ProductOption>,
|
||||
findOptions_.options as MikroOptions<ProductOption>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductOption> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductOption[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
ProductOption,
|
||||
findOptions_.where as MikroFilterQuery<ProductOption>,
|
||||
findOptions_.options as MikroOptions<ProductOption>
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: (ProductTypes.CreateProductOptionDTO & { product: { id: string } })[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductOption[]> {
|
||||
const options = data.map((option) => {
|
||||
return (manager as SqlEntityManager).create(ProductOption, option)
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(options)
|
||||
|
||||
return options
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,135 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { ProductTag } from "@models"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import { Product, ProductTag } from "@models"
|
||||
import { Context, CreateProductTagDTO, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductTagRepository implements DAL.RepositoryService {
|
||||
export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find<T = ProductTag>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<T[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductTag> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductTag[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.find(
|
||||
return await manager.find(
|
||||
ProductTag,
|
||||
findOptions_.where as MikroFilterQuery<ProductTag>,
|
||||
findOptions_.options as MikroOptions<ProductTag>
|
||||
)) as unknown as T[]
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount<T = ProductTag>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<[T[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductTag> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductTag[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.findAndCount(
|
||||
return await manager.findAndCount(
|
||||
ProductTag,
|
||||
findOptions_.where as MikroFilterQuery<ProductTag>,
|
||||
findOptions_.options as MikroOptions<ProductTag>
|
||||
)) as unknown as [T[], number]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async upsert(
|
||||
tags: CreateProductTagDTO[],
|
||||
@MedusaContext()
|
||||
context: Context = {}
|
||||
): Promise<ProductTag[]> {
|
||||
const { transactionManager: manager } = context
|
||||
|
||||
const tagsValues = tags.map((tag) => tag.value)
|
||||
const existingTags = await this.find(
|
||||
{
|
||||
where: {
|
||||
value: {
|
||||
$in: tagsValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingTagsMap = new Map(
|
||||
existingTags.map<[string, ProductTag]>((tag) => [tag.value, tag])
|
||||
)
|
||||
|
||||
const upsertedTags: ProductTag[] = []
|
||||
const tagsToCreate: RequiredEntityData<ProductTag>[] = []
|
||||
|
||||
tags.forEach((tag) => {
|
||||
const aTag = existingTagsMap.get(tag.value)
|
||||
if (aTag) {
|
||||
upsertedTags.push(aTag)
|
||||
} else {
|
||||
const newTag = (manager as SqlEntityManager).create(ProductTag, tag)
|
||||
tagsToCreate.push(newTag)
|
||||
}
|
||||
})
|
||||
|
||||
if (tagsToCreate.length) {
|
||||
const newTags: ProductTag[] = []
|
||||
tagsToCreate.forEach((tag) => {
|
||||
newTags.push((manager as SqlEntityManager).create(ProductTag, tag))
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(newTags)
|
||||
upsertedTags.push(...newTags)
|
||||
}
|
||||
|
||||
return upsertedTags
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductTag[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product, ProductType } from "@models"
|
||||
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductType> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductType[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
ProductType,
|
||||
findOptions_.where as MikroFilterQuery<ProductType>,
|
||||
findOptions_.options as MikroOptions<ProductType>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductType> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductType[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
ProductType,
|
||||
findOptions_.where as MikroFilterQuery<ProductType>,
|
||||
findOptions_.options as MikroOptions<ProductType>
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async upsert(
|
||||
types: CreateProductTypeDTO[],
|
||||
@MedusaContext()
|
||||
context: Context = {}
|
||||
): Promise<ProductType[]> {
|
||||
const { transactionManager: manager } = context
|
||||
|
||||
const typesValues = types.map((type) => type.value)
|
||||
const existingTypes = await this.find(
|
||||
{
|
||||
where: {
|
||||
value: {
|
||||
$in: typesValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingTypesMap = new Map(
|
||||
existingTypes.map<[string, ProductType]>((type) => [type.value, type])
|
||||
)
|
||||
|
||||
const upsertedTypes: ProductType[] = []
|
||||
const typesToCreate: RequiredEntityData<ProductType>[] = []
|
||||
|
||||
types.forEach((type) => {
|
||||
const aType = existingTypesMap.get(type.value)
|
||||
if (aType) {
|
||||
upsertedTypes.push(aType)
|
||||
} else {
|
||||
const newType = (manager as SqlEntityManager).create(ProductType, type)
|
||||
typesToCreate.push(newType)
|
||||
}
|
||||
})
|
||||
|
||||
if (typesToCreate.length) {
|
||||
const newTypes: ProductType[] = []
|
||||
typesToCreate.forEach((type) => {
|
||||
newTypes.push((manager as SqlEntityManager).create(ProductType, type))
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(newTypes)
|
||||
upsertedTypes.push(...newTypes)
|
||||
}
|
||||
|
||||
return upsertedTypes
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductType[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,92 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { ProductVariant } from "@models"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import { Product, ProductVariant } from "@models"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
export class ProductVariantRepository implements DAL.RepositoryService {
|
||||
export class ProductVariantRepository extends AbstractBaseRepository<ProductVariant> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find<T = ProductVariant>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<T[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductVariant[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.find(
|
||||
return await manager.find(
|
||||
ProductVariant,
|
||||
findOptions_.where as MikroFilterQuery<ProductVariant>,
|
||||
findOptions_.options as MikroOptions<ProductVariant>
|
||||
)) as unknown as T[]
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount<T = ProductVariant>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<[T[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductVariant[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.findAndCount(
|
||||
return await manager.findAndCount(
|
||||
ProductVariant,
|
||||
findOptions_.where as MikroFilterQuery<ProductVariant>,
|
||||
findOptions_.options as MikroOptions<ProductVariant>
|
||||
)) as unknown as [T[], number]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: RequiredEntityData<ProductVariant>[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductVariant[]> {
|
||||
const variants = data.map((variant) => {
|
||||
return (manager as SqlEntityManager).create(ProductVariant, variant)
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(variants)
|
||||
|
||||
return variants
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Product } from "@models"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
ProductTypes,
|
||||
WithRequiredProperty,
|
||||
} from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
export class ProductRepository extends AbstractBaseRepository<Product> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<Product[]> {
|
||||
// Spread is used to cssopy the options in case of manipulation to prevent side effects
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
@@ -38,7 +39,7 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
|
||||
await this.mutateNotInCategoriesConstraints(findOptions_)
|
||||
|
||||
return await this.manager_.find(
|
||||
return await manager.find(
|
||||
Product,
|
||||
findOptions_.where as MikroFilterQuery<Product>,
|
||||
findOptions_.options as MikroOptions<Product>
|
||||
@@ -47,20 +48,13 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<[Product[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
if (context.transactionManager) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transactionManager })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
@@ -75,17 +69,20 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
findOptions_.options as MikroOptions<Product>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to be able to have a strict not in categories, and prevent a product
|
||||
* to be return in the case it also belongs to other categories, we need to
|
||||
* first find all products that are in the categories, and then exclude them
|
||||
*/
|
||||
private async mutateNotInCategoriesConstraints(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} }
|
||||
protected async mutateNotInCategoriesConstraints(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<void> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
if (findOptions.where.categories?.id?.["$nin"]) {
|
||||
const productsInCategories = await this.manager_.find(
|
||||
const productsInCategories = await manager.find(
|
||||
Product,
|
||||
{
|
||||
categories: {
|
||||
@@ -109,4 +106,33 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: WithRequiredProperty<ProductTypes.CreateProductOnlyDTO, "status">[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<Product[]> {
|
||||
console.log((this as any).prototype)
|
||||
const products = data.map((product) => {
|
||||
return (manager as SqlEntityManager).create(Product, product)
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(products)
|
||||
|
||||
return products
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { LoaderOptions, Logger } from "@medusajs/types"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { createConnection } from "../utils"
|
||||
import * as ProductModels from "@models"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* This script is only valid for mikro orm managers. If a user provide a custom manager
|
||||
@@ -19,14 +16,14 @@ export async function revertMigration({
|
||||
logger,
|
||||
}: Pick<
|
||||
LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
"options" | "logger"
|
||||
> = {}) {
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
|
||||
const orm = await createConnection(dbData, entities)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { LoaderOptions, Logger } from "@medusajs/types"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { createConnection } from "../utils"
|
||||
import * as ProductModels from "@models"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* This script is only valid for mikro orm managers. If a user provide a custom manager
|
||||
@@ -19,14 +16,14 @@ export async function runMigrations({
|
||||
logger,
|
||||
}: Pick<
|
||||
LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
"options" | "logger"
|
||||
> = {}) {
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
|
||||
const orm = await createConnection(dbData, entities)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { createConnection } from "../utils"
|
||||
import * as ProductModels from "@models"
|
||||
import { Product, ProductCategory, ProductVariant } from "@models"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { LoaderOptions, Logger } from "@medusajs/types"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { EOL } from "os"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { resolve } from "path"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
export async function run({
|
||||
options,
|
||||
@@ -18,8 +15,8 @@ export async function run({
|
||||
}: Partial<
|
||||
Pick<
|
||||
LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
"options" | "logger"
|
||||
>
|
||||
@@ -38,7 +35,7 @@ export async function run({
|
||||
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
|
||||
const orm = await createConnection(dbData, entities)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
import { ProductService } from "@services"
|
||||
|
||||
export const nonExistingProductId = "non-existing-id"
|
||||
|
||||
export const mockContainer = createContainer()
|
||||
mockContainer.register({
|
||||
transaction: asValue(async (task) => await task()),
|
||||
productRepository: asValue({
|
||||
find: jest.fn().mockImplementation(async ({ where: { id } }) => {
|
||||
if (id === nonExistingProductId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{}]
|
||||
}),
|
||||
findAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
}),
|
||||
productService: asClass(ProductService),
|
||||
})
|
||||
@@ -1,29 +1,65 @@
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
import { ProductService } from "@services"
|
||||
|
||||
const container = createContainer()
|
||||
container.register({
|
||||
productRepository: asValue({
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
findAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
}),
|
||||
productVariantService: asValue({
|
||||
list: jest.fn().mockResolvedValue([]),
|
||||
}),
|
||||
productTagService: asValue({
|
||||
list: jest.fn().mockResolvedValue([]),
|
||||
}),
|
||||
productService: asClass(ProductService),
|
||||
})
|
||||
import { mockContainer, nonExistingProductId } from "../__fixtures__/product"
|
||||
|
||||
describe("Product service", function () {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should retrieve a product", async function () {
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const productId = "existing-product"
|
||||
await productService.retrieve(productId)
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to retrieve a product", async function () {
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const err = await productService
|
||||
.retrieve(nonExistingProductId)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: nonExistingProductId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(err.message).toBe(
|
||||
`Product with id: ${nonExistingProductId} was not found`
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {}
|
||||
const config = {
|
||||
@@ -32,27 +68,31 @@ describe("Product service", function () {
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: [],
|
||||
@@ -60,33 +100,37 @@ describe("Product service", function () {
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"]
|
||||
}
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
},
|
||||
})
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters and relations", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
@@ -94,20 +138,62 @@ describe("Product service", function () {
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"]
|
||||
}
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
populate: ["tags"],
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should list and count the products with filters and relations", async function () {
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
}
|
||||
|
||||
await productService.listAndCount(filters, config)
|
||||
|
||||
expect(productRepository.findAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,3 +4,6 @@ export { default as ProductTagService } from "./product-tag"
|
||||
export { default as ProductVariantService } from "./product-variant"
|
||||
export { default as ProductCollectionService } from "./product-collection"
|
||||
export { default as ProductCategoryService } from "./product-category"
|
||||
export { default as ProductTypeService } from "./product-type"
|
||||
export { default as ProductOptionService } from "./product-option"
|
||||
export { default as ProductImageService } from "./product-image"
|
||||
|
||||
@@ -1,34 +1,99 @@
|
||||
import { ProductCategory } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils, MedusaError, isDefined } from "@medusajs/utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productCategoryRepository: DAL.RepositoryService
|
||||
productCategoryRepository: DAL.TreeRepositoryService
|
||||
}
|
||||
|
||||
export default class ProductCategoryService<TEntity = ProductCategory> {
|
||||
protected readonly productCategoryRepository_: DAL.RepositoryService
|
||||
export default class ProductCategoryService<
|
||||
TEntity extends ProductCategory = ProductCategory
|
||||
> {
|
||||
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
|
||||
|
||||
constructor({ productCategoryRepository }: InjectedDependencies) {
|
||||
this.productCategoryRepository_ = productCategoryRepository
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productCategoryId: string,
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
if (!isDefined(productCategoryId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"productCategoryId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>({
|
||||
id: productCategoryId,
|
||||
}, config)
|
||||
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: true,
|
||||
}
|
||||
|
||||
const productCategories = await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!productCategories?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`ProductCategory with id: ${productCategoryId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return productCategories[0] as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return await this.productCategoryRepository_.find(
|
||||
return (await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return (await this.productCategoryRepository_.findAndCount(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,74 @@
|
||||
import { ProductCollection } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils, retrieveEntity } from "@medusajs/utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productCollectionRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductCollectionService<TEntity = ProductCollection> {
|
||||
protected readonly productCollectionRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductCollectionService<
|
||||
TEntity extends ProductCollection = ProductCollection
|
||||
> {
|
||||
protected readonly productCollectionRepository_: DAL.TreeRepositoryService
|
||||
|
||||
constructor({ productCollectionRepository }: InjectedDependencies) {
|
||||
this.productCollectionRepository_ = productCollectionRepository
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productCollectionId: string,
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
return (await retrieveEntity<
|
||||
ProductCollection,
|
||||
ProductTypes.ProductCollectionDTO
|
||||
>({
|
||||
id: productCollectionId,
|
||||
entityName: ProductCollection.name,
|
||||
repository: this.productCollectionRepository_,
|
||||
config,
|
||||
sharedContext,
|
||||
})) as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return (await this.productCollectionRepository_.find(
|
||||
this.buildListQueryOptions(filters, config),
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
return (await this.productCollectionRepository_.findAndCount(
|
||||
this.buildListQueryOptions(filters, config),
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
protected buildListQueryOptions(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {}
|
||||
) {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCollection>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
|
||||
queryOptions.where ??= {}
|
||||
|
||||
if (filters.title) {
|
||||
queryOptions.where["title"] = { $like: filters.title }
|
||||
}
|
||||
|
||||
return await this.productCollectionRepository_.find(queryOptions)
|
||||
return queryOptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Image } from "@models"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductImageRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productImageRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductImageService<TEntity extends Image = Image> {
|
||||
protected readonly productImageRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productImageRepository }: InjectedDependencies) {
|
||||
this.productImageRepository_ = productImageRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productImageRepository_")
|
||||
async upsert(
|
||||
urls: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productImageRepository_ as ProductImageRepository)
|
||||
.upsert!(urls, sharedContext)) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,105 @@
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductOptionService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
ProductVariantService,
|
||||
} from "@services"
|
||||
import {
|
||||
Image,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import { FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import {
|
||||
Context,
|
||||
CreateProductOnlyDTO,
|
||||
DAL,
|
||||
FindConfig,
|
||||
InternalModuleDeclaration,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import ProductImageService from "./product-image"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
isString,
|
||||
kebabCase,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
productService: ProductService<any>
|
||||
productVariantService: ProductVariantService<any>
|
||||
productVariantService: ProductVariantService<any, any>
|
||||
productTagService: ProductTagService<any>
|
||||
productCategoryService: ProductCategoryService<any>
|
||||
productCollectionService: ProductCollectionService<any>
|
||||
productImageService: ProductImageService<any>
|
||||
productTypeService: ProductTypeService<any>
|
||||
productOptionService: ProductOptionService<any>
|
||||
}
|
||||
|
||||
export default class ProductModuleService<
|
||||
TProduct = Product,
|
||||
TProductVariant = ProductVariant,
|
||||
TProductTag = ProductTag,
|
||||
TProductCollection = ProductCollection,
|
||||
TProductCategory = ProductCategory
|
||||
> implements
|
||||
ProductTypes.IProductModuleService<
|
||||
TProduct,
|
||||
TProductVariant,
|
||||
TProductTag,
|
||||
TProductCollection,
|
||||
TProductCategory
|
||||
>
|
||||
TProduct extends Product = Product,
|
||||
TProductVariant extends ProductVariant = ProductVariant,
|
||||
TProductTag extends ProductTag = ProductTag,
|
||||
TProductCollection extends ProductCollection = ProductCollection,
|
||||
TProductCategory extends ProductCategory = ProductCategory,
|
||||
TProductImage extends Image = Image,
|
||||
TProductType extends ProductType = ProductType,
|
||||
TProductOption extends ProductOption = ProductOption
|
||||
> implements ProductTypes.IProductModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly productService_: ProductService<TProduct>
|
||||
protected readonly productVariantService: ProductVariantService<TProductVariant>
|
||||
protected readonly productCategoryService: ProductCategoryService<TProductCategory>
|
||||
protected readonly productTagService: ProductTagService<TProductTag>
|
||||
protected readonly productCollectionService: ProductCollectionService<TProductCollection>
|
||||
protected readonly productVariantService_: ProductVariantService<
|
||||
TProductVariant,
|
||||
TProduct
|
||||
>
|
||||
protected readonly productCategoryService_: ProductCategoryService<TProductCategory>
|
||||
protected readonly productTagService_: ProductTagService<TProductTag>
|
||||
protected readonly productCollectionService_: ProductCollectionService<TProductCollection>
|
||||
protected readonly productImageService_: ProductImageService<TProductImage>
|
||||
protected readonly productTypeService_: ProductTypeService<TProductType>
|
||||
protected readonly productOptionService_: ProductOptionService<TProductOption>
|
||||
|
||||
constructor({
|
||||
productService,
|
||||
productVariantService,
|
||||
productTagService,
|
||||
productCategoryService,
|
||||
productCollectionService,
|
||||
}: InjectedDependencies) {
|
||||
constructor(
|
||||
{
|
||||
baseRepository,
|
||||
productService,
|
||||
productVariantService,
|
||||
productTagService,
|
||||
productCategoryService,
|
||||
productCollectionService,
|
||||
productImageService,
|
||||
productTypeService,
|
||||
productOptionService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
this.baseRepository_ = baseRepository
|
||||
this.productService_ = productService
|
||||
this.productVariantService = productVariantService
|
||||
this.productTagService = productTagService
|
||||
this.productCategoryService = productCategoryService
|
||||
this.productCollectionService = productCollectionService
|
||||
this.productVariantService_ = productVariantService
|
||||
this.productTagService_ = productTagService
|
||||
this.productCategoryService_ = productCategoryService
|
||||
this.productCollectionService_ = productCollectionService
|
||||
this.productImageService_ = productImageService
|
||||
this.productTypeService_ = productTypeService
|
||||
this.productOptionService_ = productOptionService
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.productService_.list(
|
||||
filters,
|
||||
@@ -71,10 +110,22 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(products))
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productId: string,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO> {
|
||||
const product = await this.productService_.retrieve(
|
||||
productId,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(product))
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductDTO[], number]> {
|
||||
const [products, count] = await this.productService_.listAndCount(
|
||||
filters,
|
||||
@@ -85,12 +136,26 @@ export default class ProductModuleService<
|
||||
return [JSON.parse(JSON.stringify(products)), count]
|
||||
}
|
||||
|
||||
async retrieveVariant(
|
||||
productVariantId: string,
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductVariantDTO> {
|
||||
const productVariant = await this.productVariantService_.retrieve(
|
||||
productVariantId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(productVariant))
|
||||
}
|
||||
|
||||
async listVariants(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductVariantDTO[]> {
|
||||
const variants = await this.productVariantService.list(
|
||||
const variants = await this.productVariantService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -99,12 +164,26 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(variants))
|
||||
}
|
||||
|
||||
async listAndCountVariants(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductVariantDTO[], number]> {
|
||||
const [variants, count] = await this.productVariantService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return [JSON.parse(JSON.stringify(variants)), count]
|
||||
}
|
||||
|
||||
async listTags(
|
||||
filters: ProductTypes.FilterableProductTagProps = {},
|
||||
config: FindConfig<ProductTypes.ProductTagDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductTagDTO[]> {
|
||||
const tags = await this.productTagService.list(
|
||||
const tags = await this.productTagService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -113,12 +192,26 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(tags))
|
||||
}
|
||||
|
||||
async retrieveCollection(
|
||||
productCollectionId: string,
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCollectionDTO> {
|
||||
const productCollection = await this.productCollectionService_.retrieve(
|
||||
productCollectionId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(productCollection))
|
||||
}
|
||||
|
||||
async listCollections(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCollectionDTO[]> {
|
||||
const collections = await this.productCollectionService.list(
|
||||
const collections = await this.productCollectionService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -127,12 +220,40 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(collections))
|
||||
}
|
||||
|
||||
async listAndCountCollections(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductCollectionDTO[], number]> {
|
||||
const collections = await this.productCollectionService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(collections))
|
||||
}
|
||||
|
||||
async retrieveCategory(
|
||||
productCategoryId: string,
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCategoryDTO> {
|
||||
const productCategory = await this.productCategoryService_.retrieve(
|
||||
productCategoryId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(productCategory))
|
||||
}
|
||||
|
||||
async listCategories(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCategoryDTO[]> {
|
||||
const categories = await this.productCategoryService.list(
|
||||
const categories = await this.productCategoryService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -140,4 +261,199 @@ export default class ProductModuleService<
|
||||
|
||||
return JSON.parse(JSON.stringify(categories))
|
||||
}
|
||||
|
||||
async listAndCountCategories(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductCategoryDTO[], number]> {
|
||||
const categories = await this.productCategoryService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(categories))
|
||||
}
|
||||
|
||||
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context) {
|
||||
const products = await this.create_(data, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
TProduct[],
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async create_(
|
||||
data: ProductTypes.CreateProductDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
const productVariantsMap = new Map<
|
||||
string,
|
||||
ProductTypes.CreateProductVariantDTO[]
|
||||
>()
|
||||
const productOptionsMap = new Map<
|
||||
string,
|
||||
ProductTypes.CreateProductOptionDTO[]
|
||||
>()
|
||||
|
||||
const productsData = await Promise.all(
|
||||
data.map(async (product) => {
|
||||
const productData = { ...product }
|
||||
if (!productData.handle) {
|
||||
productData.handle = kebabCase(product.title)
|
||||
}
|
||||
|
||||
const variants = productData.variants
|
||||
const options = productData.options
|
||||
delete productData.options
|
||||
delete productData.variants
|
||||
|
||||
productVariantsMap.set(productData.handle!, variants ?? [])
|
||||
productOptionsMap.set(productData.handle!, options ?? [])
|
||||
|
||||
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.is_giftcard) {
|
||||
productData.discountable = false
|
||||
}
|
||||
|
||||
if (productData.images?.length) {
|
||||
productData.images = await this.productImageService_.upsert(
|
||||
productData.images.map((image) =>
|
||||
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_id = (
|
||||
await this.productTypeService_.upsert(
|
||||
[productData.type as ProductTypes.CreateProductTypeDTO],
|
||||
sharedContext
|
||||
)
|
||||
)?.[0]!.id
|
||||
}
|
||||
|
||||
return productData as CreateProductOnlyDTO
|
||||
})
|
||||
)
|
||||
|
||||
const products = await this.productService_.create(
|
||||
productsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const productByHandleMap = new Map<string, TProduct>(
|
||||
products.map((product) => [product.handle!, product])
|
||||
)
|
||||
|
||||
const productOptionsData = [...productOptionsMap]
|
||||
.map(([handle, options]) => {
|
||||
return options.map((option) => {
|
||||
return {
|
||||
...option,
|
||||
product: productByHandleMap.get(handle)!,
|
||||
}
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
|
||||
const productOptions = await this.productOptionService_.create(
|
||||
productOptionsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
for (const variants of productVariantsMap.values()) {
|
||||
variants.forEach((variant) => {
|
||||
variant.options = variant.options?.map((option, index) => {
|
||||
const productOption = productOptions[index]
|
||||
return {
|
||||
option: productOption,
|
||||
value: option.value,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[...productVariantsMap].map(async ([handle, variants]) => {
|
||||
return await this.productVariantService_.create(
|
||||
productByHandleMap.get(handle)!,
|
||||
variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[],
|
||||
sharedContext
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async delete(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productService_.delete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
async softDelete(
|
||||
productIds: string[],
|
||||
sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.softDelete_(productIds, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
TProduct[],
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async softDelete_(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
return await this.productService_.softDelete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
async restore(
|
||||
productIds: string[],
|
||||
sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.restore_(productIds, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
TProduct[],
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async restore_(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
return await this.productService_.restore(productIds, sharedContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ProductOption } from "@models"
|
||||
import { Context, DAL, ProductTypes } from "@medusajs/types"
|
||||
import { ProductOptionRepository } from "@repositories"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productOptionRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductOptionService<
|
||||
TEntity extends ProductOption = ProductOption
|
||||
> {
|
||||
protected readonly productOptionRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productOptionRepository }: InjectedDependencies) {
|
||||
this.productOptionRepository_ =
|
||||
productOptionRepository as ProductOptionRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productOptionRepository_")
|
||||
async create(
|
||||
data: ProductTypes.CreateProductOptionOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (
|
||||
this.productOptionRepository_ as ProductOptionRepository
|
||||
).create(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
import { ProductTag } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import {
|
||||
Context,
|
||||
CreateProductTagDTO,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductTagRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productTagRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductTagService<TEntity = ProductTag> {
|
||||
protected readonly productTagRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductTagService<
|
||||
TEntity extends ProductTag = ProductTag
|
||||
> {
|
||||
protected readonly productTagRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productTagRepository }: InjectedDependencies) {
|
||||
this.productTagRepository_ = productTagRepository
|
||||
@@ -16,14 +30,28 @@ export default class ProductTagService<TEntity = ProductTag> {
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductTagProps = {},
|
||||
config: FindConfig<ProductTypes.ProductTagDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductTag>(filters, config)
|
||||
|
||||
if (filters.value) {
|
||||
queryOptions.where["value"] = { $ilike: filters.value }
|
||||
}
|
||||
|
||||
return await this.productTagRepository_.find(queryOptions)
|
||||
return (await this.productTagRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productTagRepository_")
|
||||
async upsert(
|
||||
tags: CreateProductTagDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productTagRepository_ as ProductTagRepository).upsert!(
|
||||
tags,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ProductType } from "@models"
|
||||
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductTypeRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productTypeRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductTypeService<
|
||||
TEntity extends ProductType = ProductType
|
||||
> {
|
||||
protected readonly productTypeRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productTypeRepository }: InjectedDependencies) {
|
||||
this.productTypeRepository_ = productTypeRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productTypeRepository_")
|
||||
async upsert(
|
||||
types: CreateProductTypeDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productTypeRepository_ as ProductTypeRepository)
|
||||
.upsert!(types, sharedContext)) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,115 @@
|
||||
import { ProductVariant } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import { Product, ProductVariant } from "@models"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
isString,
|
||||
MedusaContext,
|
||||
ModulesSdkUtils,
|
||||
retrieveEntity,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import ProductService from "./product"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductVariantRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productVariantRepository: DAL.RepositoryService
|
||||
productService: ProductService<any>
|
||||
}
|
||||
|
||||
export default class ProductVariantService<TEntity = ProductVariant> {
|
||||
protected readonly productVariantRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductVariantService<
|
||||
TEntity extends ProductVariant = ProductVariant,
|
||||
TProduct extends Product = Product
|
||||
> {
|
||||
protected readonly productVariantRepository_: DAL.RepositoryService
|
||||
protected readonly productService_: ProductService<TProduct>
|
||||
|
||||
constructor({ productVariantRepository }: InjectedDependencies) {
|
||||
constructor({
|
||||
productVariantRepository,
|
||||
productService,
|
||||
}: InjectedDependencies) {
|
||||
this.productVariantRepository_ = productVariantRepository
|
||||
this.productService_ = productService
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productVariantId: string,
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
return (await retrieveEntity<
|
||||
ProductVariant,
|
||||
ProductTypes.ProductVariantDTO
|
||||
>({
|
||||
id: productVariantId,
|
||||
entityName: ProductVariant.name,
|
||||
repository: this.productVariantRepository_,
|
||||
config,
|
||||
sharedContext,
|
||||
})) as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return await this.productVariantRepository_.find(queryOptions)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
|
||||
return (await this.productVariantRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
|
||||
return (await this.productVariantRepository_.findAndCount(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||
async create(
|
||||
productOrId: TProduct | string,
|
||||
data: ProductTypes.CreateProductVariantOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
let product = productOrId as unknown as Product
|
||||
|
||||
if (isString(productOrId)) {
|
||||
product = await this.productService_.retrieve(
|
||||
productOrId as string,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
let computedRank = product.variants.toArray().length
|
||||
|
||||
const data_ = [...data]
|
||||
data_.forEach((variant) => {
|
||||
Object.assign(variant, {
|
||||
variant_rank: computedRank++,
|
||||
product,
|
||||
})
|
||||
})
|
||||
|
||||
return (await (
|
||||
this.productVariantRepository_ as ProductVariantRepository
|
||||
).create(data_, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})) as TEntity[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,55 @@
|
||||
import { ProductTagService, ProductVariantService } from "@services"
|
||||
import { Product } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ProductStatus,
|
||||
ProductTypes,
|
||||
WithRequiredProperty,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { ProductRepository } from "@repositories"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productRepository: DAL.RepositoryService
|
||||
productVariantService: ProductVariantService
|
||||
productTagService: ProductTagService
|
||||
}
|
||||
|
||||
export default class ProductService<TEntity = Product> {
|
||||
protected readonly productRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductService<TEntity extends Product = Product> {
|
||||
protected readonly productRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productRepository }: InjectedDependencies) {
|
||||
this.productRepository_ = productRepository
|
||||
}
|
||||
|
||||
async retrieve(productId: string, sharedContext?: Context): Promise<TEntity> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
||||
id: productId,
|
||||
})
|
||||
const product = await this.productRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!product?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with id: ${productId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return product[0] as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
if (filters.category_ids) {
|
||||
if (Array.isArray(filters.category_ids)) {
|
||||
@@ -34,14 +64,17 @@ export default class ProductService<TEntity = Product> {
|
||||
delete filters.category_ids
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return await this.productRepository_.find(queryOptions)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
|
||||
return (await this.productRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
if (filters.category_ids) {
|
||||
if (Array.isArray(filters.category_ids)) {
|
||||
@@ -56,7 +89,60 @@ export default class ProductService<TEntity = Product> {
|
||||
delete filters.category_ids
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return await this.productRepository_.findAndCount(queryOptions)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
|
||||
return (await this.productRepository_.findAndCount(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async create(
|
||||
data: ProductTypes.CreateProductOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
data.forEach((product) => {
|
||||
product.status ??= ProductStatus.DRAFT
|
||||
})
|
||||
|
||||
return (await (this.productRepository_ as ProductRepository).create(
|
||||
data as WithRequiredProperty<
|
||||
ProductTypes.CreateProductOnlyDTO,
|
||||
"status"
|
||||
>[],
|
||||
{
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productRepository_.delete(ids, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async softDelete(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this.productRepository_.softDelete(productIds, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async restore(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this.productRepository_.restore(productIds, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { Constructor, DAL, IEventBusService } from "@medusajs/types"
|
||||
|
||||
export type ProductServiceInitializeOptions = {
|
||||
database: {
|
||||
clientUrl: string
|
||||
schema?: string
|
||||
driverOptions?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export type ProductServiceInitializeCustomDataLayerOptions = {
|
||||
manager?: any
|
||||
repositories?: { [key: string]: Constructor<DAL.RepositoryService> }
|
||||
}
|
||||
import { IEventBusService } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
eventBusService?: IEventBusService
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { MikroORM, PostgreSqlDriver } from "@mikro-orm/postgresql"
|
||||
import { ProductServiceInitializeOptions } from "../types"
|
||||
import { ModuleServiceInitializeOptions } from "@medusajs/types"
|
||||
|
||||
export async function createConnection(
|
||||
database: ProductServiceInitializeOptions["database"],
|
||||
database: ModuleServiceInitializeOptions["database"],
|
||||
entities: any[]
|
||||
) {
|
||||
const schema = database.schema || "public"
|
||||
const orm = await MikroORM.init<PostgreSqlDriver>({
|
||||
discovery: { disableDynamicFileAccess: true },
|
||||
entities,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false,
|
||||
baseDir: process.cwd(),
|
||||
clientUrl: database.clientUrl,
|
||||
schema,
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export * from "./query"
|
||||
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
|
||||
|
||||
export * from "./create-connection"
|
||||
export * from "./load-database-config"
|
||||
export * from "./soft-deletable"
|
||||
|
||||
export function shouldForceTransaction(target: any): boolean {
|
||||
return target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
}
|
||||
|
||||
export function doNotForceTransaction(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// TODO: Should we create a mikro orm specific package for this and the base repository?
|
||||
|
||||
import { Filter } from "@mikro-orm/core"
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
interface FilterArguments {
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
export const SoftDeletable = (): ClassDecorator => {
|
||||
return Filter({
|
||||
name: DAL.SoftDeletableFilterKey,
|
||||
cond: ({ withDeleted }: FilterArguments = {}) => {
|
||||
if (withDeleted) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
deleted_at: null,
|
||||
}
|
||||
},
|
||||
default: true,
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "1.8.10",
|
||||
"description": "Medusa Types definition",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
|
||||
@@ -41,11 +41,12 @@ export type Writable<T> = {
|
||||
}
|
||||
|
||||
export interface FindConfig<Entity> {
|
||||
select?: (keyof Entity)[]
|
||||
select?: (keyof Entity | string)[]
|
||||
skip?: number
|
||||
take?: number
|
||||
relations?: string[]
|
||||
order?: { [K: string]: "ASC" | "DESC" }
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
export type ExtendedFindConfig<TEntity> = (
|
||||
|
||||
@@ -21,4 +21,6 @@ export type FindOptions<T = any> = {
|
||||
options?: OptionsQuery<T, any>
|
||||
}
|
||||
|
||||
export const SoftDeletableFilterKey = "softDeletable"
|
||||
|
||||
export * from "./repository-service"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FindOptions } from "./index"
|
||||
import { RepositoryTransformOptions } from "../common"
|
||||
import { Context } from "../shared-context"
|
||||
|
||||
/**
|
||||
* Data access layer (DAL) interface to implements for any repository service.
|
||||
@@ -7,6 +8,54 @@ import { RepositoryTransformOptions } from "../common"
|
||||
* ORM directly and allows to switch to another ORM without changing the business logic.
|
||||
*/
|
||||
export interface RepositoryService<T = any> {
|
||||
find(options?: FindOptions<T>, transformOptions?: RepositoryTransformOptions): Promise<T[]>
|
||||
findAndCount(options?: FindOptions<T>, transformOptions?: RepositoryTransformOptions): Promise<[T[], number]>
|
||||
transaction(
|
||||
task: (transactionManager: unknown) => Promise<any>,
|
||||
context?: {
|
||||
isolationLevel?: string
|
||||
transaction?: unknown
|
||||
enableNestedTransactions?: boolean
|
||||
}
|
||||
): Promise<any>
|
||||
|
||||
serialize<TData extends object, TResult extends object, TOptions = any>(
|
||||
data: TData,
|
||||
options?: TOptions
|
||||
): Promise<TResult>
|
||||
|
||||
serialize<TData extends object[], TResult extends object[], TOptions = any>(
|
||||
data: TData[],
|
||||
options?: TOptions
|
||||
): Promise<TResult>
|
||||
|
||||
find(options?: FindOptions<T>, context?: Context): Promise<T[]>
|
||||
|
||||
findAndCount(
|
||||
options?: FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
// Only required for some repositories
|
||||
upsert?(data: any, context?: Context): Promise<T[]>
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
softDelete(ids: string[], context?: Context): Promise<T[]>
|
||||
|
||||
restore(ids: string[], context?: Context): Promise<T[]>
|
||||
}
|
||||
|
||||
export interface TreeRepositoryService<T = any> extends RepositoryService<T> {
|
||||
find(
|
||||
options?: FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<T[]>
|
||||
|
||||
findAndCount(
|
||||
options?: FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface IInventoryService {
|
||||
context?: SharedContext
|
||||
): Promise<ReservationItemDTO>
|
||||
|
||||
// TODO make it bulk
|
||||
createInventoryItem(
|
||||
input: CreateInventoryItemInput,
|
||||
context?: SharedContext
|
||||
@@ -95,6 +96,7 @@ export interface IInventoryService {
|
||||
context?: SharedContext
|
||||
): Promise<void>
|
||||
|
||||
// TODO make it bulk
|
||||
deleteInventoryItem(
|
||||
inventoryItemId: string,
|
||||
context?: SharedContext
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MedusaContainer } from "../common"
|
||||
import { Logger } from "../logger"
|
||||
import { RepositoryService } from "../dal"
|
||||
|
||||
export type Constructor<T> = new (...args: any[]) => T
|
||||
export * from "../common/medusa-container"
|
||||
@@ -93,3 +94,17 @@ export type ModuleExports = {
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
export interface ModuleServiceInitializeOptions {
|
||||
database: {
|
||||
clientUrl: string
|
||||
schema?: string
|
||||
driverOptions?: Record<string, unknown>
|
||||
debug?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type ModuleServiceInitializeCustomDataLayerOptions = {
|
||||
manager?: any
|
||||
repositories?: { [key: string]: Constructor<RepositoryService> }
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ProductDTO {
|
||||
tags: ProductTagDTO[]
|
||||
variants: ProductVariantDTO[]
|
||||
options: ProductOptionDTO[]
|
||||
images: ProductImageDTO[]
|
||||
discountable?: boolean
|
||||
external_id?: string | null
|
||||
created_at?: string | Date
|
||||
@@ -86,7 +87,7 @@ export interface ProductTagDTO {
|
||||
id: string
|
||||
value: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
products: ProductDTO[]
|
||||
products?: ProductDTO[]
|
||||
}
|
||||
|
||||
export interface ProductCollectionDTO {
|
||||
@@ -95,6 +96,7 @@ export interface ProductCollectionDTO {
|
||||
handle: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
deleted_at?: string | Date
|
||||
products?: ProductDTO[]
|
||||
}
|
||||
|
||||
export interface ProductTypeDTO {
|
||||
@@ -113,6 +115,13 @@ export interface ProductOptionDTO {
|
||||
deleted_at?: string | Date
|
||||
}
|
||||
|
||||
export interface ProductImageDTO {
|
||||
id: string
|
||||
url: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
deleted_at?: string | Date
|
||||
}
|
||||
|
||||
export interface ProductOptionValueDTO {
|
||||
id: string
|
||||
value: string
|
||||
@@ -164,3 +173,126 @@ export interface FilterableProductCategoryProps
|
||||
is_internal?: boolean
|
||||
include_descendants_tree?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Write DTO (module API input)
|
||||
*/
|
||||
|
||||
export interface CreateProductTypeDTO {
|
||||
id?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CreateProductTagDTO {
|
||||
id?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CreateProductOptionDTO {
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface CreateProductVariantOptionDTO {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CreateProductVariantDTO {
|
||||
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 {
|
||||
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
|
||||
collection_id?: string
|
||||
tags?: CreateProductTagDTO[]
|
||||
// sales_channel
|
||||
categories?: { id: string }[]
|
||||
options?: CreateProductOptionDTO[]
|
||||
variants?: CreateProductVariantDTO[]
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
export interface CreateProductVariantOnlyDTO {
|
||||
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 {
|
||||
product: { id: string }
|
||||
title: string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
CreateProductDTO,
|
||||
FilterableProductCategoryProps,
|
||||
FilterableProductCollectionProps,
|
||||
FilterableProductProps,
|
||||
@@ -11,48 +12,94 @@ import {
|
||||
ProductVariantDTO,
|
||||
} from "./common"
|
||||
import { FindConfig } from "../common"
|
||||
import { SharedContext } from "../shared-context"
|
||||
import { Context } from "../shared-context"
|
||||
|
||||
export interface IProductModuleService {
|
||||
retrieve(productId: string, sharedContext?: Context): Promise<ProductDTO>
|
||||
|
||||
export interface IProductModuleService<
|
||||
TProduct = any,
|
||||
TProductVariant = any,
|
||||
TProductTag = any,
|
||||
TProductCollection = any,
|
||||
TProductCategory = any
|
||||
> {
|
||||
list(
|
||||
filters?: FilterableProductProps,
|
||||
config?: FindConfig<ProductDTO>,
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductDTO[]>
|
||||
|
||||
listAndCount(
|
||||
filters?: FilterableProductProps,
|
||||
config?: FindConfig<ProductDTO>,
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductDTO[], number]>
|
||||
|
||||
listTags(
|
||||
filters?: FilterableProductTagProps,
|
||||
config?: FindConfig<ProductTagDTO>,
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTagDTO[]>
|
||||
|
||||
retrieveVariant(
|
||||
productVariantId: string,
|
||||
config?: FindConfig<ProductVariantDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductVariantDTO>
|
||||
|
||||
listVariants(
|
||||
filters?: FilterableProductVariantProps,
|
||||
config?: FindConfig<ProductVariantDTO>,
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductVariantDTO[]>
|
||||
|
||||
listAndCountVariants(
|
||||
filters?: FilterableProductVariantProps,
|
||||
config?: FindConfig<ProductVariantDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductVariantDTO[], number]>
|
||||
|
||||
retrieveCollection(
|
||||
productCollectionId: string,
|
||||
config?: FindConfig<ProductCollectionDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCollectionDTO>
|
||||
|
||||
listCollections(
|
||||
filters?: FilterableProductCollectionProps,
|
||||
config?: FindConfig<ProductCollectionDTO>,
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCollectionDTO[]>
|
||||
|
||||
listAndCountCollections(
|
||||
filters?: FilterableProductCollectionProps,
|
||||
config?: FindConfig<ProductCollectionDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductCollectionDTO[], number]>
|
||||
|
||||
retrieveCategory(
|
||||
productCategoryId: string,
|
||||
config?: FindConfig<ProductCategoryDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCategoryDTO>
|
||||
|
||||
listCategories(
|
||||
filters?: FilterableProductCategoryProps,
|
||||
config?: FindConfig<ProductCategoryDTO>,
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCategoryDTO[]>
|
||||
|
||||
listAndCountCategories(
|
||||
filters?: FilterableProductCategoryProps,
|
||||
config?: FindConfig<ProductCategoryDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductCategoryDTO[], number]>
|
||||
|
||||
create(
|
||||
data: CreateProductDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<ProductDTO[]>
|
||||
|
||||
delete(productIds: string[], sharedContext?: Context): Promise<void>
|
||||
|
||||
softDelete(
|
||||
productIds: string[],
|
||||
sharedContext?: Context
|
||||
): Promise<ProductDTO[]>
|
||||
|
||||
restore(productIds: string[], sharedContext?: Context): Promise<ProductDTO[]>
|
||||
}
|
||||
|
||||
@@ -3,3 +3,9 @@ import { EntityManager } from "typeorm"
|
||||
export type SharedContext = {
|
||||
transactionManager?: EntityManager
|
||||
}
|
||||
|
||||
export type Context<TManager = unknown> = {
|
||||
transactionManager?: TManager
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "1.9.2",
|
||||
"description": "Medusa utilities functions shared by Medusa core and Modules",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
@@ -22,6 +23,7 @@
|
||||
"cross-env": "^5.2.1",
|
||||
"express": "^4.18.2",
|
||||
"jest": "^25.5.4",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
},
|
||||
@@ -32,7 +34,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||
"build": "tsc --build",
|
||||
"build": "rimraf dist && tsc --build",
|
||||
"watch": "tsc --build --watch",
|
||||
"test": "jest"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * as DecoratorUtils from "./decorators";
|
||||
export * as EventBusUtils from "./event-bus";
|
||||
export * as SearchUtils from "./search";
|
||||
|
||||
export * as DecoratorUtils from "./decorators"
|
||||
export * as EventBusUtils from "./event-bus"
|
||||
export * as SearchUtils from "./search"
|
||||
export * as ModulesSdkUtils from "./modules-sdk"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function deduplicate<T = any>(collection: T[]): T[] {
|
||||
return [...new Set(collection)]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./build-query"
|
||||
export * from "./deduplicate"
|
||||
export * from "./errors"
|
||||
export * from "./generate-entity-id"
|
||||
export * from "./get-config-file"
|
||||
|
||||
@@ -4,16 +4,7 @@ export function MedusaContext() {
|
||||
propertyKey: string | symbol,
|
||||
parameterIndex: number
|
||||
) {
|
||||
if (!target.MedusaContextIndex_) {
|
||||
target.MedusaContextIndex_ = {}
|
||||
}
|
||||
|
||||
if (propertyKey in target.MedusaContextIndex_) {
|
||||
throw new Error(
|
||||
`Only one MedusaContext is allowed on method "${String(propertyKey)}".`
|
||||
)
|
||||
}
|
||||
|
||||
target.MedusaContextIndex_ ??= {}
|
||||
target.MedusaContextIndex_[propertyKey] = parameterIndex
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SharedContext } from "@medusajs/types"
|
||||
import { Context, SharedContext } from "@medusajs/types"
|
||||
|
||||
export function InjectEntityManager(
|
||||
shouldForceTransaction: (target: any) => boolean = () => false,
|
||||
managerProperty: string = "manager_"
|
||||
managerProperty: string | false = "manager_"
|
||||
): MethodDecorator {
|
||||
return function (
|
||||
target: any,
|
||||
@@ -20,18 +20,27 @@ export function InjectEntityManager(
|
||||
const argIndex = target.MedusaContextIndex_[propertyKey]
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const shouldForceTransactionRes = shouldForceTransaction(target)
|
||||
const context: SharedContext = args[argIndex] ?? {}
|
||||
const context: SharedContext | Context = args[argIndex] ?? {}
|
||||
|
||||
if (!shouldForceTransactionRes && context?.transactionManager) {
|
||||
return await originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
return await this[managerProperty].transaction(
|
||||
return await (managerProperty === false
|
||||
? this
|
||||
: this[managerProperty]
|
||||
).transaction(
|
||||
async (transactionManager) => {
|
||||
args[argIndex] = args[argIndex] ?? {}
|
||||
args[argIndex].transactionManager = transactionManager
|
||||
|
||||
return await originalMethod.apply(this, args)
|
||||
},
|
||||
{
|
||||
transaction: context?.transactionManager,
|
||||
isolationLevel: (context as Context)?.isolationLevel,
|
||||
enableNestedTransactions:
|
||||
(context as Context).enableNestedTransactions ?? false,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ export * from "./cli"
|
||||
export * from "./common"
|
||||
export * from "./decorators"
|
||||
export * from "./event-bus"
|
||||
export * from "./search"
|
||||
export * from "./search"
|
||||
export * from "./modules-sdk"
|
||||
|
||||
+14
-8
@@ -1,4 +1,4 @@
|
||||
import { loadDatabaseConfig } from "../load-database-config"
|
||||
import { loadDatabaseConfig } from "../load-module-database-config"
|
||||
|
||||
describe("loadDatabaseConfig", function () {
|
||||
afterEach(() => {
|
||||
@@ -8,7 +8,7 @@ describe("loadDatabaseConfig", function () {
|
||||
|
||||
it("should return the local configuration using the environment variable", function () {
|
||||
process.env.POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
let config = loadDatabaseConfig()
|
||||
let config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.POSTGRES_URL,
|
||||
@@ -17,12 +17,13 @@ describe("loadDatabaseConfig", function () {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
|
||||
delete process.env.POSTGRES_URL
|
||||
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
config = loadDatabaseConfig()
|
||||
config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||
@@ -31,13 +32,14 @@ describe("loadDatabaseConfig", function () {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return the remote configuration using the environment variable", function () {
|
||||
process.env.POSTGRES_URL = "postgres://https://test.com:5432/medusa"
|
||||
let config = loadDatabaseConfig()
|
||||
let config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.POSTGRES_URL,
|
||||
@@ -48,12 +50,13 @@ describe("loadDatabaseConfig", function () {
|
||||
},
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
|
||||
delete process.env.POSTGRES_URL
|
||||
process.env.PRODUCT_POSTGRES_URL = "postgres://https://test.com:5432/medusa"
|
||||
config = loadDatabaseConfig()
|
||||
config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||
@@ -64,6 +67,7 @@ describe("loadDatabaseConfig", function () {
|
||||
},
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
@@ -76,7 +80,7 @@ describe("loadDatabaseConfig", function () {
|
||||
},
|
||||
}
|
||||
|
||||
const config = loadDatabaseConfig(options)
|
||||
let config = loadDatabaseConfig("product", options)
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: options.database.clientUrl,
|
||||
@@ -85,6 +89,7 @@ describe("loadDatabaseConfig", function () {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
@@ -97,7 +102,7 @@ describe("loadDatabaseConfig", function () {
|
||||
},
|
||||
}
|
||||
|
||||
const config = loadDatabaseConfig(options)
|
||||
let config = loadDatabaseConfig("product", options)
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: options.database.clientUrl,
|
||||
@@ -108,6 +113,7 @@ describe("loadDatabaseConfig", function () {
|
||||
},
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
@@ -115,7 +121,7 @@ describe("loadDatabaseConfig", function () {
|
||||
it("should throw if no clientUrl is provided", function () {
|
||||
let error
|
||||
try {
|
||||
loadDatabaseConfig()
|
||||
loadDatabaseConfig("product")
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
+14
-14
@@ -1,12 +1,5 @@
|
||||
/**
|
||||
* Move to a new build query utils
|
||||
*/
|
||||
import { DAL, FindConfig } from "@medusajs/types"
|
||||
import { isObject } from "@medusajs/utils"
|
||||
|
||||
export function deduplicateIfNecessary<T = any>(collection: T | T[]) {
|
||||
return Array.isArray(collection) ? [...new Set(collection)] : collection
|
||||
}
|
||||
import { DAL, FindConfig, SoftDeletableFilterKey } from "@medusajs/types"
|
||||
import { deduplicate, isObject } from "../common"
|
||||
|
||||
export function buildQuery<T = any, TDto = any>(
|
||||
filters: Record<string, any> = {},
|
||||
@@ -16,11 +9,18 @@ export function buildQuery<T = any, TDto = any>(
|
||||
buildWhere(filters, where)
|
||||
|
||||
const findOptions: DAL.OptionsQuery<T, any> = {
|
||||
populate: config.relations ?? [],
|
||||
fields: config.select,
|
||||
limit: config.take,
|
||||
populate: deduplicate(config.relations ?? []),
|
||||
fields: config.select as string[],
|
||||
limit: config.take ?? 15,
|
||||
offset: config.skip,
|
||||
} as any
|
||||
}
|
||||
|
||||
if (config.withDeleted) {
|
||||
findOptions.filters ??= {}
|
||||
findOptions.filters[SoftDeletableFilterKey] = {
|
||||
withDeleted: true,
|
||||
}
|
||||
}
|
||||
|
||||
return { where, options: findOptions }
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function buildQuery<T = any, TDto = any>(
|
||||
function buildWhere(filters: Record<string, any> = {}, where = {}) {
|
||||
for (let [prop, value] of Object.entries(filters)) {
|
||||
if (Array.isArray(value)) {
|
||||
value = deduplicateIfNecessary(value)
|
||||
value = deduplicate(value)
|
||||
where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value }
|
||||
continue
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./inject-transaction-manager"
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Context, SharedContext } from "@medusajs/types"
|
||||
|
||||
export function InjectTransactionManager(
|
||||
shouldForceTransaction: (target: any) => boolean = () => false,
|
||||
managerProperty?: string
|
||||
): MethodDecorator {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string | symbol,
|
||||
descriptor: any
|
||||
): void {
|
||||
if (!target.MedusaContextIndex_) {
|
||||
throw new Error(
|
||||
`To apply @InjectTransactionManager you have to flag a parameter using @MedusaContext`
|
||||
)
|
||||
}
|
||||
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
const argIndex = target.MedusaContextIndex_[propertyKey]
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const shouldForceTransactionRes = shouldForceTransaction(target)
|
||||
const context: SharedContext | Context = args[argIndex] ?? {}
|
||||
|
||||
if (!shouldForceTransactionRes && context?.transactionManager) {
|
||||
return await originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
return await (!managerProperty
|
||||
? this
|
||||
: this[managerProperty]
|
||||
).transaction(
|
||||
async (transactionManager) => {
|
||||
args[argIndex] = args[argIndex] ?? {}
|
||||
args[argIndex].transactionManager = transactionManager
|
||||
|
||||
return await originalMethod.apply(this, args)
|
||||
},
|
||||
{
|
||||
transaction: context?.transactionManager,
|
||||
isolationLevel: (context as Context)?.isolationLevel,
|
||||
enableNestedTransactions:
|
||||
(context as Context).enableNestedTransactions ?? false,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./load-module-database-config"
|
||||
export * from "./decorators"
|
||||
export * from "./build-query"
|
||||
export * from "./retrieve-entity"
|
||||
+23
-23
@@ -1,23 +1,19 @@
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { MedusaError } from "../common"
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
|
||||
function getEnv(key: string): string {
|
||||
const value = process.env[`PRODUCT_${key}`] ?? process.env[`${key}`]
|
||||
function getEnv(key: string, moduleName: string): string {
|
||||
const value =
|
||||
process.env[`${moduleName.toUpperCase()}_${key}`] ?? process.env[`${key}`]
|
||||
return value ?? ""
|
||||
}
|
||||
|
||||
function isProductServiceInitializeOptions(
|
||||
function isModuleServiceInitializeOptions(
|
||||
obj: unknown
|
||||
): obj is ProductServiceInitializeOptions {
|
||||
return !!(obj as ProductServiceInitializeOptions)?.database
|
||||
): obj is ModulesSdkTypes.ModuleServiceInitializeOptions {
|
||||
return !!(obj as any)?.database
|
||||
}
|
||||
|
||||
function getDefaultDriverOptions(
|
||||
clientUrl: string
|
||||
): ProductServiceInitializeOptions["database"]["driverOptions"] {
|
||||
function getDefaultDriverOptions(clientUrl: string) {
|
||||
const localOptions = {
|
||||
connection: {
|
||||
ssl: false,
|
||||
@@ -45,31 +41,35 @@ function getDefaultDriverOptions(
|
||||
|
||||
/**
|
||||
* Load the config for the database connection. The options can be retrieved
|
||||
* through PRODUCT_* (e.g PRODUCT_POSTGRES_URL) or * (e.g POSTGRES_URL) environment variables or the options object.
|
||||
* e.g through PRODUCT_* (e.g PRODUCT_POSTGRES_URL) or * (e.g POSTGRES_URL) environment variables or the options object.
|
||||
* @param options
|
||||
* @param moduleName
|
||||
*/
|
||||
export function loadDatabaseConfig(
|
||||
moduleName: string,
|
||||
options?:
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
): ProductServiceInitializeOptions["database"] {
|
||||
const clientUrl = getEnv("POSTGRES_URL")
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
): ModulesSdkTypes.ModuleServiceInitializeOptions["database"] {
|
||||
const clientUrl = getEnv("POSTGRES_URL", moduleName)
|
||||
|
||||
const database: ProductServiceInitializeOptions["database"] = {
|
||||
clientUrl: getEnv("POSTGRES_URL"),
|
||||
schema: getEnv("POSTGRES_SCHEMA") ?? "public",
|
||||
const database = {
|
||||
clientUrl: getEnv("POSTGRES_URL", moduleName),
|
||||
schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public",
|
||||
driverOptions: JSON.parse(
|
||||
getEnv("POSTGRES_DRIVER_OPTIONS") ||
|
||||
getEnv("POSTGRES_DRIVER_OPTIONS", moduleName) ||
|
||||
JSON.stringify(getDefaultDriverOptions(clientUrl))
|
||||
),
|
||||
debug: process.env.NODE_ENV?.startsWith("dev") ?? false,
|
||||
}
|
||||
|
||||
if (isProductServiceInitializeOptions(options)) {
|
||||
if (isModuleServiceInitializeOptions(options)) {
|
||||
database.clientUrl = options.database.clientUrl ?? database.clientUrl
|
||||
database.schema = options.database.schema ?? database.schema
|
||||
database.driverOptions =
|
||||
options.database.driverOptions ??
|
||||
getDefaultDriverOptions(database.clientUrl)
|
||||
database.debug = options.database.debug ?? database.debug
|
||||
}
|
||||
|
||||
if (!database.clientUrl) {
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FindConfig, DAL, Context } from "@medusajs/types"
|
||||
import { MedusaError, isDefined, lowerCaseFirst } from "../common"
|
||||
import { buildQuery } from "./build-query"
|
||||
|
||||
type RetrieveEntityParams<TDTO> = {
|
||||
id: string,
|
||||
entityName: string,
|
||||
repository: DAL.TreeRepositoryService
|
||||
config: FindConfig<TDTO>
|
||||
sharedContext?: Context
|
||||
}
|
||||
|
||||
export async function retrieveEntity<
|
||||
TEntity,
|
||||
TDTO,
|
||||
>({
|
||||
id,
|
||||
entityName,
|
||||
repository,
|
||||
config = {},
|
||||
sharedContext,
|
||||
}: RetrieveEntityParams<TDTO>): Promise<TEntity> {
|
||||
if (!isDefined(id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"${lowerCaseFirst(entityName)}Id" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery<TEntity>({
|
||||
id,
|
||||
}, config)
|
||||
|
||||
const entities = await repository.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!entities?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${entityName} with id: ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return entities[0]
|
||||
}
|
||||
Reference in New Issue
Block a user