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:
authored and GitHub committed 2023-07-16 20:19:23 +02:00
1 parent 5b91a3503a
commit befc2f1c80
98 files changed
+5407 -651

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"@medusajs/product": patch
---
Feat(product): product module create - delete - soft delete - restore - create workflow
+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/product": patch
"@medusajs/types": patch
---
chore: added collection methods for module and collection service
+62
View File
@@ -65,6 +65,68 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: yarn test run: yarn test
integration-tests-packages:
needs: setup
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 1s
--health-timeout 10s
--health-retries 10
ports:
- 6379:6379
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 1s
--health-timeout 10s
--health-retries 10
ports:
- 5432:5432
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
- name: Checkout
uses: actions/checkout@v2.3.5
with:
fetch-depth: 0
- name: Setup Node.js environment
uses: actions/setup-node@v2.4.1
with:
node-version: "16"
cache: "yarn"
- name: Install dependencies
uses: ./.github/actions/cache-deps
with:
extension: pipeline
- name: Build Packages
run: yarn build
- name: Run integration tests
run: yarn test:integration:packages
env:
DB_PASSWORD: postgres
DB_USERNAME: postgres
SPLIT: ${{ steps['split-tests'].outputs['split'] }}
integration-tests-api: integration-tests-api:
needs: setup needs: setup
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -569,7 +569,8 @@ describe("Inventory Items endpoints", () => {
]) ])
) )
expect(response.data.inventory_items).toHaveLength(3) expect(response.data.inventory_items).toHaveLength(3)
expect(response.data.inventory_items).toEqual([ expect(response.data.inventory_items).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
sku: "Test Sku", sku: "Test Sku",
}), }),
@@ -580,6 +581,7 @@ describe("Inventory Items endpoints", () => {
title: "Test Item", title: "Test Item",
}), }),
]) ])
)
}) })
}) })
+1 -1
View File
@@ -74,7 +74,7 @@
"prettier": "prettier", "prettier": "prettier",
"jest": "jest", "jest": "jest",
"test": "turbo run test --no-daemon", "test": "turbo run test --no-daemon",
"test:integration": "turbo run test:integration --no-daemon --filter='./packages/*'", "test:integration:packages": "turbo run test:integration --no-daemon --filter='./packages/*'",
"test:integration:api": "turbo run test:integration --no-daemon --filter=integration-tests-api", "test:integration:api": "turbo run test:integration --no-daemon --filter=integration-tests-api",
"test:integration:plugins": "turbo run test:integration --no-daemon --filter=integration-tests-plugins", "test:integration:plugins": "turbo run test:integration --no-daemon --filter=integration-tests-plugins",
"test:integration:repositories": "turbo run test:integration --no-daemon --filter=integration-tests-repositories", "test:integration:repositories": "turbo run test:integration --no-daemon --filter=integration-tests-repositories",
@@ -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 { class CustomRepository extends BaseRepository {
constructor() {} constructor({ manager }) {
// @ts-ignore
find(options?: FindOptions): Promise<any[]> { super(...arguments)
throw new Error("Method not implemented.")
}
findAndCount(options?: FindOptions): Promise<[any[], number]> {
throw new Error("Method not implemented.")
} }
} }
@@ -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 { SqlEntityManager } from "@mikro-orm/postgresql"
import { import {
Image,
Product, Product,
ProductCategory, ProductCategory,
ProductCollection, ProductCollection,
@@ -7,9 +9,16 @@ import {
} from "@models" } from "@models"
import ProductOption from "../../../src/models/product-option" import ProductOption from "../../../src/models/product-option"
export * from "./data/create-product"
export async function createProductAndTags( export async function createProductAndTags(
manager: SqlEntityManager, manager: SqlEntityManager,
data: any[] data: {
id?: string
title: string
status: ProductTypes.ProductStatus
tags?: { id: string; value: string }[]
}[]
) { ) {
const products: any[] = data.map((productData) => { const products: any[] = data.map((productData) => {
return manager.create(Product, productData) return manager.create(Product, productData)
@@ -35,7 +44,11 @@ export async function createProductVariants(
export async function createCollections( export async function createCollections(
manager: SqlEntityManager, manager: SqlEntityManager,
collectionData: any[] collectionData: {
id?: string
title: string
handle?: string
}[]
) { ) {
const collections: any[] = collectionData.map((collectionData) => { const collections: any[] = collectionData.map((collectionData) => {
return manager.create(ProductCollection, collectionData) return manager.create(ProductCollection, collectionData)
@@ -48,10 +61,21 @@ export async function createCollections(
export async function createOptions( export async function createOptions(
manager: SqlEntityManager, 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) => { const options: any[] = optionsData.map((option) => {
return manager.create(ProductOption, o) return manager.create(ProductOption, option)
}) })
await manager.persistAndFlush(options) await manager.persistAndFlush(options)
@@ -59,6 +83,19 @@ export async function createOptions(
return options 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( export async function assignCategoriesToProduct(
manager: SqlEntityManager, manager: SqlEntityManager,
product: Product, 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 { MedusaModule } from "@medusajs/modules-sdk"
import { Product } from "@models"
import { initialize } from "../../src" import { initialize } from "../../src"
import * as CustomRepositories from "../__fixtures__/module" import * as CustomRepositories from "../__fixtures__/module"
import { ProductRepository } from "../__fixtures__/module" import { ProductRepository } from "../__fixtures__/module"
import { createProductAndTags } from "../__fixtures__/product" import { createProductAndTags } from "../__fixtures__/product"
import { productsData } from "../__fixtures__/product/data" import { productsData } from "../__fixtures__/product/data"
import { DB_URL, TestDatabase } from "../utils" 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 () => { const beforeEach_ = async () => {
await TestDatabase.setupDatabase() await TestDatabase.setupDatabase()
@@ -18,12 +20,11 @@ const afterEach_ = async () => {
describe("Product module", function () { describe("Product module", function () {
describe("Using built-in data access layer", function () { describe("Using built-in data access layer", function () {
let module let module: IProductModuleService
let products: Product[]
beforeEach(async () => { beforeEach(async () => {
const testManager = await beforeEach_() const testManager = await beforeEach_()
products = await createProductAndTags(testManager, productsData) await createProductAndTags(testManager, productsData)
module = await initialize({ module = await initialize({
database: { database: {
@@ -46,13 +47,12 @@ describe("Product module", function () {
}) })
describe("Using custom data access layer", function () { describe("Using custom data access layer", function () {
let module let module: IProductModuleService
let products: Product[]
beforeEach(async () => { beforeEach(async () => {
const testManager = await beforeEach_() const testManager = await beforeEach_()
products = await createProductAndTags(testManager, productsData) await createProductAndTags(testManager, productsData)
module = await initialize({ module = await initialize({
database: { database: {
@@ -78,12 +78,11 @@ describe("Product module", function () {
}) })
describe("Using custom data access layer and connection", function () { describe("Using custom data access layer and connection", function () {
let module let module: IProductModuleService
let products: Product[]
beforeEach(async () => { beforeEach(async () => {
const testManager = await beforeEach_() const testManager = await beforeEach_()
products = await createProductAndTags(testManager, productsData) await createProductAndTags(testManager, productsData)
MedusaModule.clearInstances() MedusaModule.clearInstances()
@@ -106,4 +105,244 @@ describe("Product module", function () {
expect(products).toHaveLength(0) 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) jest.setTimeout(30000)
describe("ProductCategory Service", () => { describe("Product category Service", () => {
let service: ProductCategoryService let service: ProductCategoryService
let testManager: SqlEntityManager let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager let repositoryManager: SqlEntityManager
@@ -47,7 +47,7 @@ describe("ProductCategory Service", () => {
const productCategoryResults = await service.list( 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, include_descendants_tree: true,
}, },
{ {
select: ["id", "handle"] as any, select: ["id", "handle"],
} }
) )
@@ -192,7 +192,7 @@ describe("ProductCategory Service", () => {
is_internal: false, 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) jest.setTimeout(30000)
describe("Product Service", () => { describe("Product collection Service", () => {
let service: ProductCollectionService let service: ProductCollectionService
let testManager: SqlEntityManager let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager let repositoryManager: SqlEntityManager
@@ -59,9 +59,9 @@ describe("Product Service", () => {
}) })
it("list product collections", async () => { it("list product collections", async () => {
const tagsResults = await service.list() const productCollectionResults = await service.list()
expect(tagsResults).toEqual([ expect(productCollectionResults).toEqual([
expect.objectContaining({ expect.objectContaining({
id: "test-1", id: "test-1",
title: "col 1", title: "col 1",
@@ -82,9 +82,9 @@ describe("Product Service", () => {
}) })
it("list product collections by id", async () => { 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({ expect.objectContaining({
id: "test-1", id: "test-1",
title: "col 1", title: "col 1",
@@ -93,9 +93,9 @@ describe("Product Service", () => {
}) })
it("list product collections by title matching string", async () => { 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({ expect.objectContaining({
id: "test-3", id: "test-3",
title: "col 3 extra", 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: []
}
)
})
})
}) })
@@ -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")
})
})
})
@@ -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")
})
})
})
@@ -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) jest.setTimeout(30000)
describe("Product Service", () => { describe("Product tag Service", () => {
let service: ProductTagService let service: ProductTagService
let testManager: SqlEntityManager let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager let repositoryManager: SqlEntityManager
@@ -1,6 +1,6 @@
import { TestDatabase } from "../../../utils" import { TestDatabase } from "../../../utils"
import { ProductVariantService } from "@services" import { ProductService, ProductVariantService } from "@services"
import { ProductVariantRepository } from "@repositories" import { ProductRepository, ProductVariantRepository } from "@repositories"
import { Product, ProductTag, ProductVariant } from "@models" import { Product, ProductTag, ProductVariant } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Collection } from "@mikro-orm/core" import { Collection } from "@mikro-orm/core"
@@ -12,6 +12,7 @@ import {
createProductVariants, createProductVariants,
} from "../../../__fixtures__/product" } from "../../../__fixtures__/product"
import { productsData, variantsData } from "../../../__fixtures__/product/data" import { productsData, variantsData } from "../../../__fixtures__/product/data"
import { buildProductVariantOnlyData } from "../../../__fixtures__/variant/data/create-variant"
describe("ProductVariant Service", () => { describe("ProductVariant Service", () => {
let service: ProductVariantService let service: ProductVariantService
@@ -20,6 +21,7 @@ describe("ProductVariant Service", () => {
let variantOne: ProductVariant let variantOne: ProductVariant
let variantTwo: ProductVariant let variantTwo: ProductVariant
let productOne: Product let productOne: Product
const productVariantTestOne = "test-1"
beforeEach(async () => { beforeEach(async () => {
await TestDatabase.setupDatabase() await TestDatabase.setupDatabase()
@@ -28,8 +30,17 @@ describe("ProductVariant Service", () => {
const productVariantRepository = new ProductVariantRepository({ const productVariantRepository = new ProductVariantRepository({
manager: repositoryManager, manager: repositoryManager,
}) })
const productRepository = new ProductRepository({
manager: repositoryManager,
})
service = new ProductVariantService({ productVariantRepository }) const productService = new ProductService({
productRepository,
})
service = new ProductVariantService({
productService,
productVariantRepository,
})
}) })
afterEach(async () => { afterEach(async () => {
@@ -47,7 +58,7 @@ describe("ProductVariant Service", () => {
}) })
variantOne = testManager.create(ProductVariant, { variantOne = testManager.create(ProductVariant, {
id: "test-1", id: productVariantTestOne,
title: "variant 1", title: "variant 1",
inventory_quantity: 10, inventory_quantity: 10,
product: productOne, product: productOne,
@@ -95,7 +106,7 @@ describe("ProductVariant Service", () => {
it("passing populate, scopes the results of the response", async () => { it("passing populate, scopes the results of the response", async () => {
const results = await service.list( const results = await service.list(
{ {
id: "test-1", id: productVariantTestOne,
}, },
{ {
select: ["id", "title", "product.title"] as any, select: ["id", "title", "product.title"] as any,
@@ -105,7 +116,7 @@ describe("ProductVariant Service", () => {
expect(results).toEqual([ expect(results).toEqual([
expect.objectContaining({ expect.objectContaining({
id: "test-1", id: productVariantTestOne,
title: "variant 1", title: "variant 1",
product: expect.objectContaining({ product: expect.objectContaining({
id: "product-1", id: "product-1",
@@ -118,7 +129,7 @@ describe("ProductVariant Service", () => {
expect(JSON.parse(JSON.stringify(results))).toEqual([ expect(JSON.parse(JSON.stringify(results))).toEqual([
{ {
id: "test-1", id: productVariantTestOne,
title: "variant 1", title: "variant 1",
product_id: "product-1", product_id: "product-1",
product: { product: {
@@ -175,11 +186,148 @@ describe("ProductVariant Service", () => {
expect(JSON.parse(JSON.stringify(variants))).toEqual([ expect(JSON.parse(JSON.stringify(variants))).toEqual([
expect.objectContaining({ expect.objectContaining({
id: "test-1", id: productVariantTestOne,
title: "variant title", title: "variant title",
sku: "sku 1", 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 { TestDatabase } from "../../../utils"
import { import { ProductService } from "@services"
ProductService,
ProductTagService,
ProductVariantService,
} from "@services"
import { ProductRepository } from "@repositories" import { ProductRepository } from "@repositories"
import { Product, ProductCategory, ProductVariant } from "@models" import { Image, Product, ProductCategory, ProductVariant } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductDTO } from "@medusajs/types" import { ProductDTO } from "@medusajs/types"
import { createProductCategories } from "../../../__fixtures__/product-category" import { createProductCategories } from "../../../__fixtures__/product-category"
import { import {
assignCategoriesToProduct, assignCategoriesToProduct,
createImages,
createProductAndTags, createProductAndTags,
createProductVariants, createProductVariants,
} from "../../../__fixtures__/product" } from "../../../__fixtures__/product"
@@ -20,13 +17,8 @@ import {
productsData, productsData,
variantsData, variantsData,
} from "../../../__fixtures__/product/data" } from "../../../__fixtures__/product/data"
import { buildProductOnlyData } from "../../../__fixtures__/product/data/create-product"
const productVariantService = { import { kebabCase } from "@medusajs/utils"
list: jest.fn(),
} as unknown as ProductVariantService
const productTagService = {
list: jest.fn(),
} as unknown as ProductTagService
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -48,8 +40,6 @@ describe("Product Service", () => {
service = new ProductService({ service = new ProductService({
productRepository, productRepository,
productVariantService,
productTagService,
}) })
}) })
@@ -57,7 +47,74 @@ describe("Product Service", () => {
await TestDatabase.clearDatabase() 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("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", () => { describe("relation: tags", () => {
beforeEach(async () => { beforeEach(async () => {
testManager = await TestDatabase.forkManager() testManager = await TestDatabase.forkManager()
@@ -65,7 +122,7 @@ describe("Product Service", () => {
products = await createProductAndTags(testManager, productsData) 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( const productsResult = await service.list(
{ {
id: products[0].id, 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({ const productsResult = await service.list({
id: products[0].id, 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( const products = await service.list(
{ {
id: workingProduct.id, 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( const products = await service.list(
{ {
id: workingProduct.id, id: workingProduct.id,
@@ -215,7 +272,7 @@ describe("Product Service", () => {
variants = await createProductVariants(testManager, variantsData) 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( const productsResult = await service.list(
{ {
id: products[0].id, 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()
})
})
}) })
+4 -4
View File
@@ -1,8 +1,8 @@
const { dropDatabase } = require("pg-god") const { dropDatabase } = require("pg-god")
const DB_HOST = process.env.DB_HOST const DB_HOST = process.env.DB_HOST ?? "localhost"
const DB_USERNAME = process.env.DB_USERNAME const DB_USERNAME = process.env.DB_USERNAME ?? "postgres"
const DB_PASSWORD = process.env.DB_PASSWORD const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
const DB_NAME = process.env.DB_TEMP_NAME const DB_NAME = process.env.DB_TEMP_NAME
const pgGodCredentials = { const pgGodCredentials = {
@@ -16,7 +16,7 @@ afterAll(async () => {
await dropDatabase({ databaseName: DB_NAME }, pgGodCredentials) await dropDatabase({ databaseName: DB_NAME }, pgGodCredentials)
} catch (e) { } catch (e) {
console.error( 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}`
) )
} }
}) })
+1
View File
@@ -39,6 +39,7 @@
"@mikro-orm/cli": "5.7.12", "@mikro-orm/cli": "5.7.12",
"@mikro-orm/migrations": "5.7.12", "@mikro-orm/migrations": "5.7.12",
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"faker": "^6.6.6",
"jest": "^25.5.4", "jest": "^25.5.4",
"medusa-test-utils": "^1.1.40", "medusa-test-utils": "^1.1.40",
"pg-god": "^1.0.12", "pg-god": "^1.0.12",
+4 -8
View File
@@ -5,18 +5,14 @@ import {
MODULE_PACKAGE_NAMES, MODULE_PACKAGE_NAMES,
Modules, Modules,
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { IProductModuleService } from "@medusajs/types" import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
import { moduleDefinition } from "../module-definition" import { moduleDefinition } from "../module-definition"
import { import { InitializeModuleInjectableDependencies } from "../types"
InitializeModuleInjectableDependencies,
ProductServiceInitializeCustomDataLayerOptions,
ProductServiceInitializeOptions,
} from "../types"
export const initialize = async ( export const initialize = async (
options?: options?:
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
| ExternalModuleDeclaration, | ExternalModuleDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<IProductModuleService> => { ): Promise<IProductModuleService> => {
+7 -10
View File
@@ -6,24 +6,21 @@ import {
MODULE_RESOURCE_TYPE, MODULE_RESOURCE_TYPE,
MODULE_SCOPE, MODULE_SCOPE,
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { MedusaError } from "@medusajs/utils" import { MedusaError, ModulesSdkUtils } from "@medusajs/utils"
import { EntitySchema } from "@mikro-orm/core" import { EntitySchema } from "@mikro-orm/core"
import * as ProductModels from "@models" import * as ProductModels from "@models"
import { import { createConnection } from "../utils"
ProductServiceInitializeCustomDataLayerOptions, import { ModulesSdkTypes } from "@medusajs/types"
ProductServiceInitializeOptions,
} from "../types"
import { createConnection, loadDatabaseConfig } from "../utils"
export default async ( export default async (
{ {
options, options,
container, container,
}: LoaderOptions< }: LoaderOptions<
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>, >,
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration
): Promise<void> => { ): Promise<void> => {
@@ -35,11 +32,11 @@ export default async (
} }
const customManager = ( const customManager = (
options as ProductServiceInitializeCustomDataLayerOptions options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
)?.manager )?.manager
if (!customManager) { if (!customManager) {
const dbData = loadDatabaseConfig(options) const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
await loadDefault({ database: dbData, container }) await loadDefault({ database: dbData, container })
} else { } else {
container.register({ container.register({
+22 -12
View File
@@ -3,36 +3,39 @@ import { LoaderOptions } from "@medusajs/modules-sdk"
import { asClass } from "awilix" import { asClass } from "awilix"
import { import {
ProductCategoryService, ProductCategoryService,
ProductCollectionService,
ProductImageService,
ProductModuleService, ProductModuleService,
ProductOptionService,
ProductService, ProductService,
ProductTagService, ProductTagService,
ProductTypeService,
ProductVariantService, ProductVariantService,
ProductCollectionService,
} from "@services" } from "@services"
import * as DefaultRepositories from "@repositories" import * as DefaultRepositories from "@repositories"
import { import {
BaseRepository,
ProductCategoryRepository, ProductCategoryRepository,
ProductCollectionRepository, ProductCollectionRepository,
ProductImageRepository,
ProductOptionRepository,
ProductRepository, ProductRepository,
ProductTagRepository, ProductTagRepository,
ProductTypeRepository,
ProductVariantRepository, ProductVariantRepository,
} from "@repositories" } from "@repositories"
import { import { Constructor, DAL, ModulesSdkTypes } from "@medusajs/types"
ProductServiceInitializeCustomDataLayerOptions,
ProductServiceInitializeOptions,
} from "../types"
import { Constructor, DAL } from "@medusajs/types"
import { lowerCaseFirst } from "@medusajs/utils" import { lowerCaseFirst } from "@medusajs/utils"
export default async ({ export default async ({
container, container,
options, options,
}: LoaderOptions< }: LoaderOptions<
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>): Promise<void> => { >): Promise<void> => {
const customRepositories = ( const customRepositories = (
options as ProductServiceInitializeCustomDataLayerOptions options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
)?.repositories )?.repositories
container.register({ container.register({
@@ -42,6 +45,9 @@ export default async ({
productVariantService: asClass(ProductVariantService).singleton(), productVariantService: asClass(ProductVariantService).singleton(),
productTagService: asClass(ProductTagService).singleton(), productTagService: asClass(ProductTagService).singleton(),
productCollectionService: asClass(ProductCollectionService).singleton(), productCollectionService: asClass(ProductCollectionService).singleton(),
productImageService: asClass(ProductImageService).singleton(),
productTypeService: asClass(ProductTypeService).singleton(),
productOptionService: asClass(ProductOptionService).singleton(),
}) })
if (customRepositories) { if (customRepositories) {
@@ -53,13 +59,17 @@ export default async ({
function loadDefaultRepositories({ container }) { function loadDefaultRepositories({ container }) {
container.register({ container.register({
productRepository: asClass(ProductRepository).singleton(), baseRepository: asClass(BaseRepository).singleton(),
productVariantRepository: asClass(ProductVariantRepository).singleton(), productImageRepository: asClass(ProductImageRepository).singleton(),
productTagRepository: asClass(ProductTagRepository).singleton(),
productCategoryRepository: asClass(ProductCategoryRepository).singleton(), productCategoryRepository: asClass(ProductCategoryRepository).singleton(),
productCollectionRepository: asClass( productCollectionRepository: asClass(
ProductCollectionRepository ProductCollectionRepository
).singleton(), ).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", "name": "product_collection",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_collection_deleted_at",
"primary": false,
"unique": false
},
{ {
"keyName": "IDX_product_collection_handle_unique", "keyName": "IDX_product_collection_handle_unique",
"columnNames": [ "columnNames": [
@@ -234,6 +243,80 @@
"checks": [], "checks": [],
"foreignKeys": {} "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": { "columns": {
"id": { "id": {
@@ -277,6 +360,15 @@
"name": "product_tag", "name": "product_tag",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_tag_deleted_at",
"primary": false,
"unique": false
},
{ {
"keyName": "product_tag_pkey", "keyName": "product_tag_pkey",
"columnNames": [ "columnNames": [
@@ -333,6 +425,15 @@
"name": "product_type", "name": "product_type",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_type_deleted_at",
"primary": false,
"unique": false
},
{ {
"keyName": "product_type_pkey", "keyName": "product_type_pkey",
"columnNames": [ "columnNames": [
@@ -588,6 +689,15 @@
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_deleted_at",
"primary": false,
"unique": false
},
{ {
"keyName": "IDX_product_handle_unique", "keyName": "IDX_product_handle_unique",
"columnNames": [ "columnNames": [
@@ -698,6 +808,15 @@
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_option_deleted_at",
"primary": false,
"unique": false
},
{ {
"keyName": "product_option_pkey", "keyName": "product_option_pkey",
"columnNames": [ "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": { "columns": {
"product_id": { "product_id": {
@@ -917,6 +1101,7 @@
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"default": "100",
"mappedType": "decimal" "mappedType": "decimal"
}, },
"allow_backorder": { "allow_backorder": {
@@ -1027,6 +1212,7 @@
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"default": "0",
"mappedType": "decimal" "mappedType": "decimal"
}, },
"created_at": { "created_at": {
@@ -1072,12 +1258,21 @@
"name": "product_variant", "name": "product_variant",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_variant_deleted_at",
"primary": false,
"unique": false
},
{ {
"columnNames": [ "columnNames": [
"product_id" "product_id"
], ],
"composite": false, "composite": false,
"keyName": "IDX_product_variant_product_id_index", "keyName": "IDX_product_variant_product_id",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
@@ -1210,7 +1405,25 @@
"option_id" "option_id"
], ],
"composite": false, "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, "primary": false,
"unique": 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;'
)
}
}
+2
View File
@@ -4,3 +4,5 @@ export { default as ProductCollection } from "./product-collection"
export { default as ProductTag } from "./product-tag" export { default as ProductTag } from "./product-tag"
export { default as ProductType } from "./product-type" export { default as ProductType } from "./product-type"
export { default as ProductVariant } from "./product-variant" 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 id!: string
@Property({ columnType: "text", nullable: false }) @Property({ columnType: "text", nullable: false })
name: string name?: string
@Property({ columnType: "text", default: "", nullable: false }) @Property({ columnType: "text", default: "", nullable: false })
description?: string description?: string
@@ -1,15 +1,26 @@
import { import {
BeforeCreate, BeforeCreate,
Collection,
Entity, Entity,
Index,
OneToMany,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
Unique, Unique,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { generateEntityId, kebabCase } from "@medusajs/utils" import { generateEntityId, kebabCase } from "@medusajs/utils"
import Product from "./product"
import { SoftDeletable } from "../utils"
type OptionalRelations = "products"
@Entity({ tableName: "product_collection" }) @Entity({ tableName: "product_collection" })
@SoftDeletable()
class ProductCollection { class ProductCollection {
[OptionalProps]?: OptionalRelations
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -21,13 +32,17 @@ class ProductCollection {
name: "IDX_product_collection_handle_unique", name: "IDX_product_collection_handle_unique",
properties: ["handle"], properties: ["handle"],
}) })
handle: string handle?: string
@OneToMany(() => Product, (product) => product.collection)
products = new Collection<Product>(this)
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Index({ name: "IDX_product_collection_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@BeforeCreate() @BeforeCreate()
onCreate() { 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 { import {
BeforeCreate, BeforeCreate,
Entity, Entity,
Index,
ManyToOne, ManyToOne,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
@@ -9,28 +11,53 @@ import { generateEntityId } from "@medusajs/utils"
import ProductOption from "./product-option" import ProductOption from "./product-option"
import { ProductVariant } from "./index" 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" }) @Entity({ tableName: "product_option_value" })
@SoftDeletable()
class ProductOptionValue { class ProductOptionValue {
[OptionalProps]?: OptionalFields | OptionalRelations
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@Property({ columnType: "text" }) @Property({ columnType: "text" })
value: string value: string
@Property({ persist: false })
option_id!: string
@ManyToOne(() => ProductOption, { @ManyToOne(() => ProductOption, {
index: "IDX_product_option_value_product_option", index: "IDX_product_option_value_option_id",
fieldName: "option_id",
}) })
option: ProductOption 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 variant: ProductVariant
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Index({ name: "IDX_product_option_value_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@BeforeCreate() @BeforeCreate()
beforeCreate() { beforeCreate() {
+14 -3
View File
@@ -3,17 +3,26 @@ import {
Cascade, Cascade,
Collection, Collection,
Entity, Entity,
Index,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils" import { generateEntityId } from "@medusajs/utils"
import { Product } from "./index" import { Product } from "./index"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
import { SoftDeletable } from "../utils"
type OptionalRelations = "values" | "product"
type OptionalFields = "product_id"
@Entity({ tableName: "product_option" }) @Entity({ tableName: "product_option" })
@SoftDeletable()
class ProductOption { class ProductOption {
[OptionalProps]?: OptionalRelations | OptionalFields
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -21,23 +30,25 @@ class ProductOption {
title: string title: string
@Property({ persist: false }) @Property({ persist: false })
product_id!: number product_id!: string
@ManyToOne(() => Product, { @ManyToOne(() => Product, {
index: "IDX_product_option_product_id", index: "IDX_product_option_product_id",
fieldName: "product_id",
}) })
product: Product product: Product
@OneToMany(() => ProductOptionValue, (value) => value.option, { @OneToMany(() => ProductOptionValue, (value) => value.option, {
cascade: [Cascade.REMOVE], cascade: [Cascade.REMOVE, "soft-remove" as any],
}) })
values = new Collection<ProductOptionValue>(this) values = new Collection<ProductOptionValue>(this)
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Index({ name: "IDX_product_option_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@BeforeCreate() @BeforeCreate()
beforeCreate() { beforeCreate() {
+10 -1
View File
@@ -2,16 +2,24 @@ import {
BeforeCreate, BeforeCreate,
Collection, Collection,
Entity, Entity,
Index,
ManyToMany, ManyToMany,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils" import { generateEntityId } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { SoftDeletable } from "../utils"
type OptionalRelations = "products"
@Entity({ tableName: "product_tag" }) @Entity({ tableName: "product_tag" })
@SoftDeletable()
class ProductTag { class ProductTag {
[OptionalProps]?: OptionalRelations
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -21,8 +29,9 @@ class ProductTag {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Index({ name: "IDX_product_tag_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@ManyToMany(() => Product, (product) => product.tags) @ManyToMany(() => Product, (product) => product.tags)
products = new Collection<Product>(this) products = new Collection<Product>(this)
+11 -2
View File
@@ -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 { generateEntityId } from "@medusajs/utils"
import { SoftDeletable } from "../utils"
@Entity({ tableName: "product_type" }) @Entity({ tableName: "product_type" })
@SoftDeletable()
class ProductType { class ProductType {
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -13,8 +21,9 @@ class ProductType {
@Property({ columnType: "json", nullable: true }) @Property({ columnType: "json", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Index({ name: "IDX_product_type_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
+12 -10
View File
@@ -3,6 +3,7 @@ import {
Cascade, Cascade,
Collection, Collection,
Entity, Entity,
Index,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
OptionalProps, OptionalProps,
@@ -13,18 +14,18 @@ import {
import { generateEntityId } from "@medusajs/utils" import { generateEntityId } from "@medusajs/utils"
import { Product } from "@models" import { Product } from "@models"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
import { SoftDeletable } from "../utils"
type OptionalFields = type OptionalFields =
| "created_at" | "created_at"
| "updated_at" | "updated_at"
| "updated_at"
| "deleted_at"
| "allow_backorder" | "allow_backorder"
| "manage_inventory" | "manage_inventory"
| "product" | "product"
| "product_id" | "product_id"
@Entity({ tableName: "product_variant" }) @Entity({ tableName: "product_variant" })
@SoftDeletable()
class ProductVariant { class ProductVariant {
[OptionalProps]?: OptionalFields [OptionalProps]?: OptionalFields
@@ -65,14 +66,14 @@ class ProductVariant {
// Note: Upon serialization, this turns to a string. This is on purpose, because you would loose // 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. // precision if you cast numeric to JS number, as JS number is a float.
// Ref: https://github.com/mikro-orm/mikro-orm/issues/2295 // Ref: https://github.com/mikro-orm/mikro-orm/issues/2295
@Property({ columnType: "numeric" }) @Property({ columnType: "numeric", default: 100 })
inventory_quantity: number inventory_quantity?: number = 100
@Property({ columnType: "boolean", default: false }) @Property({ columnType: "boolean", default: false })
allow_backorder: boolean allow_backorder?: boolean = false
@Property({ columnType: "boolean", default: true }) @Property({ columnType: "boolean", default: true })
manage_inventory: boolean manage_inventory?: boolean = true
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
hs_code?: string | null hs_code?: string | null
@@ -101,7 +102,7 @@ class ProductVariant {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({ columnType: "numeric", nullable: true }) @Property({ columnType: "numeric", nullable: true, default: 0 })
variant_rank?: number | null variant_rank?: number | null
@Property({ persist: false }) @Property({ persist: false })
@@ -117,18 +118,19 @@ class ProductVariant {
}) })
updated_at: Date updated_at: Date
@Index({ name: "IDX_product_variant_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@ManyToOne(() => Product, { @ManyToOne(() => Product, {
onDelete: "cascade", onDelete: "cascade",
index: "IDX_product_variant_product_id_index", index: "IDX_product_variant_product_id",
fieldName: "product_id", fieldName: "product_id",
}) })
product!: Product product!: Product
@OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, { @OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, {
cascade: [Cascade.PERSIST, Cascade.REMOVE], cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any],
}) })
options = new Collection<ProductOptionValue>(this) options = new Collection<ProductOptionValue>(this)
+35 -8
View File
@@ -3,6 +3,7 @@ import {
Collection, Collection,
Entity, Entity,
Enum, Enum,
Index,
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
@@ -20,16 +21,20 @@ import ProductOption from "./product-option"
import ProductTag from "./product-tag" import ProductTag from "./product-tag"
import ProductType from "./product-type" import ProductType from "./product-type"
import ProductVariant from "./product-variant" import ProductVariant from "./product-variant"
import ProductImage from "./product-image"
import { SoftDeletable } from "../utils"
type OptionalRelations = "collection" | "type" type OptionalRelations = "collection" | "type"
type OptionalFields = type OptionalFields =
| "collection_id"
| "type_id"
| "is_giftcard" | "is_giftcard"
| "discountable" | "discountable"
| "created_at" | "created_at"
| "updated_at" | "updated_at"
| "deleted_at"
@Entity({ tableName: "product" }) @Entity({ tableName: "product" })
@SoftDeletable()
class Product { class Product {
[OptionalProps]?: OptionalRelations | OptionalFields [OptionalProps]?: OptionalRelations | OptionalFields
@@ -58,16 +63,17 @@ class Product {
@Enum(() => ProductTypes.ProductStatus) @Enum(() => ProductTypes.ProductStatus)
status!: ProductTypes.ProductStatus status!: ProductTypes.ProductStatus
// TODO: add images model
// images: Image[]
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
thumbnail?: string | null thumbnail?: string | null
@OneToMany(() => ProductOption, (o) => o.product) @OneToMany(() => ProductOption, (o) => o.product, {
cascade: ["soft-remove"] as any,
})
options = new Collection<ProductOption>(this) 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) variants = new Collection<ProductVariant>(this)
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
@@ -94,12 +100,22 @@ class Product {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
material?: string | null material?: string | null
@ManyToOne(() => ProductCollection, { nullable: true }) @Property({ persist: false })
collection_id!: string
@ManyToOne(() => ProductCollection, {
nullable: true,
fieldName: "collection_id",
})
collection!: ProductCollection collection!: ProductCollection
@Property({ persist: false })
type_id!: string
@ManyToOne(() => ProductType, { @ManyToOne(() => ProductType, {
nullable: true, nullable: true,
index: "IDX_product_type_id", index: "IDX_product_type_id",
fieldName: "type_id",
}) })
type!: ProductType type!: ProductType
@@ -107,12 +123,22 @@ class Product {
owner: true, owner: true,
pivotTable: "product_tags", pivotTable: "product_tags",
index: "IDX_product_tag_id", index: "IDX_product_tag_id",
cascade: ["soft-remove"] as any,
}) })
tags = new Collection<ProductTag>(this) 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", { @ManyToMany(() => ProductCategory, "products", {
owner: true, owner: true,
pivotTable: "product_category_product", pivotTable: "product_category_product",
cascade: ["soft-remove"] as any,
}) })
categories = new Collection<ProductCategory>(this) categories = new Collection<ProductCategory>(this)
@@ -132,8 +158,9 @@ class Product {
}) })
updated_at: Date updated_at: Date
@Index({ name: "IDX_product_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date deleted_at?: Date
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@@ -3,7 +3,6 @@ import { ProductModuleService } from "@services"
import loadContainer from "./loaders/container" import loadContainer from "./loaders/container"
import loadConnection from "./loaders/connection" import loadConnection from "./loaders/connection"
import * as ProductModels from "@models" import * as ProductModels from "@models"
import { revertMigration, runMigrations } from "./scripts"
const service = ProductModuleService const service = ProductModuleService
const loaders = [loadContainer, loadConnection] as any const loaders = [loadContainer, loadConnection] as any
@@ -13,6 +12,4 @@ export const moduleDefinition: ModuleExports = {
service, service,
loaders, loaders,
models, models,
runMigrations,
revertMigration,
} }
+243
View File
@@ -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 { ProductRepository } from "./product"
export { ProductTagRepository } from "./product-tag" export { ProductTagRepository } from "./product-tag"
export { ProductVariantRepository } from "./product-variant" export { ProductVariantRepository } from "./product-variant"
export { ProductCollectionRepository } from "./product-collection" export { ProductCollectionRepository } from "./product-collection"
export { ProductCategoryRepository } from "./product-category" 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 { import {
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy, LoadStrategy,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { deduplicateIfNecessary } from "../utils" import { Product, ProductCategory } from "@models"
import { ProductCategory } from "@models" import { Context, DAL, ProductCategoryTransformOptions } from "@medusajs/types"
import { DAL, ProductCategoryTransformOptions } from "@medusajs/types"
import groupBy from "lodash/groupBy" import groupBy from "lodash/groupBy"
import { AbstractTreeRepositoryBase } from "./base"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
export class ProductCategoryRepository export class ProductCategoryRepository extends AbstractTreeRepositoryBase<ProductCategory> {
implements DAL.RepositoryService<ProductCategory>
{
protected readonly manager_: SqlEntityManager protected readonly manager_: SqlEntityManager
constructor({ manager }) { constructor({ manager }: { manager: SqlEntityManager }) {
this.manager_ = manager.fork() // @ts-ignore
super(...arguments)
this.manager_ = manager
} }
async find( async find(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }, findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {}, transformOptions: ProductCategoryTransformOptions = {},
context: { transaction?: any } = {} context: Context = {}
): Promise<ProductCategory[]> { ): 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 findOptions_ = { ...findOptions }
const { includeDescendantsTree } = transformOptions const { includeDescendantsTree } = transformOptions
findOptions_.options ??= {} findOptions_.options ??= {}
const fields = (findOptions_.options.fields ??= []) const fields = (findOptions_.options.fields ??= [])
findOptions_.options.limit ??= 15
// Ref: Building descendants // Ref: Building descendants
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately // 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") 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
const productCategories = await this.manager_.find( const productCategories = await manager.find(
ProductCategory, ProductCategory,
findOptions_.where as MikroFilterQuery<ProductCategory>, findOptions_.where as MikroFilterQuery<ProductCategory>,
findOptions_.options as MikroOptions<ProductCategory> findOptions_.options as MikroOptions<ProductCategory>
@@ -69,8 +62,12 @@ export class ProductCategoryRepository
async buildProductCategoriesWithDescendants( async buildProductCategoriesWithDescendants(
productCategories: ProductCategory[], productCategories: ProductCategory[],
findOptions: DAL.FindOptions<ProductCategory> = { where: {} } findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
context: Context = {}
): Promise<ProductCategory[]> { ): Promise<ProductCategory[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
for (let productCategory of productCategories) { for (let productCategory of productCategories) {
const whereOptions = { const whereOptions = {
...findOptions.where, ...findOptions.where,
@@ -78,9 +75,11 @@ export class ProductCategoryRepository
$like: `${productCategory.mpath}%`, $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, ProductCategory,
whereOptions as MikroFilterQuery<ProductCategory>, whereOptions as MikroFilterQuery<ProductCategory>,
findOptions.options as MikroOptions<ProductCategory> findOptions.options as MikroOptions<ProductCategory>
@@ -111,30 +110,64 @@ export class ProductCategoryRepository
async findAndCount( async findAndCount(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }, findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {}, transformOptions: ProductCategoryTransformOptions = {},
context: { transaction?: any } = {} context: Context = {}
): Promise<[ProductCategory[], number]> { ): 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 findOptions_ = { ...findOptions }
const { includeDescendantsTree } = transformOptions
findOptions_.options ??= {} findOptions_.options ??= {}
findOptions_.options.limit ??= 15 const fields = (findOptions_.options.fields ??= [])
if (findOptions_.options.populate) { // Ref: Building descendants
deduplicateIfNecessary(findOptions_.options.populate) // 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")
if (context.transaction) { fields.indexOf("parent_category_id") === -1 &&
Object.assign(findOptions_.options, { ctx: context.transaction }) fields.push("parent_category_id")
} }
Object.assign(findOptions_.options, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return await this.manager_.findAndCount( const [productCategories, count] = await manager.findAndCount(
ProductCategory, ProductCategory,
findOptions_.where as MikroFilterQuery<ProductCategory>, findOptions_.where as MikroFilterQuery<ProductCategory>,
findOptions_.options as MikroOptions<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 { Product, ProductCollection } from "@models"
import { ProductCollection } from "@models"
import { import {
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy, LoadStrategy,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { deduplicateIfNecessary } from "../utils" import { Context, DAL } from "@medusajs/types"
import { 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 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>( async find(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<ProductCollection> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<T[]> { ): Promise<ProductCollection[]> {
// 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 findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return (await this.manager_.find( return await manager.find(
ProductCollection, ProductCollection,
findOptions_.where as MikroFilterQuery<ProductCollection>, findOptions_.where as MikroFilterQuery<ProductCollection>,
findOptions_.options as MikroOptions<ProductCollection> findOptions_.options as MikroOptions<ProductCollection>
)) as unknown as T[] )
} }
async findAndCount<T = ProductCollection>( async findAndCount(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<ProductCollection> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<[T[], number]> { ): Promise<[ProductCollection[], 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 findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return (await this.manager_.findAndCount( return await manager.findAndCount(
ProductCollection, ProductCollection,
findOptions_.where as MikroFilterQuery<ProductCollection>, findOptions_.where as MikroFilterQuery<ProductCollection>,
findOptions_.options as MikroOptions<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
}
}
+102 -41
View File
@@ -1,74 +1,135 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { import {
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy, LoadStrategy,
RequiredEntityData,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { deduplicateIfNecessary } from "../utils" import { Product, ProductTag } from "@models"
import { ProductTag } from "@models" import { Context, CreateProductTagDTO, DAL } from "@medusajs/types"
import { 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 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>( async find(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<ProductTag> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<T[]> { ): Promise<ProductTag[]> {
// 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 findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return (await this.manager_.find( return await manager.find(
ProductTag, ProductTag,
findOptions_.where as MikroFilterQuery<ProductTag>, findOptions_.where as MikroFilterQuery<ProductTag>,
findOptions_.options as MikroOptions<ProductTag> findOptions_.options as MikroOptions<ProductTag>
)) as unknown as T[] )
} }
async findAndCount<T = ProductTag>( async findAndCount(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<ProductTag> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<[T[], number]> { ): Promise<[ProductTag[], 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 findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return (await this.manager_.findAndCount( return await manager.findAndCount(
ProductTag, ProductTag,
findOptions_.where as MikroFilterQuery<ProductTag>, findOptions_.where as MikroFilterQuery<ProductTag>,
findOptions_.options as MikroOptions<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 { import {
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy, LoadStrategy,
RequiredEntityData,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { deduplicateIfNecessary } from "../utils" import { Product, ProductVariant } from "@models"
import { ProductVariant } from "@models" import { Context, DAL } from "@medusajs/types"
import { 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 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>( async find(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<T[]> { ): Promise<ProductVariant[]> {
// 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 findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return (await this.manager_.find( return await manager.find(
ProductVariant, ProductVariant,
findOptions_.where as MikroFilterQuery<ProductVariant>, findOptions_.where as MikroFilterQuery<ProductVariant>,
findOptions_.options as MikroOptions<ProductVariant> findOptions_.options as MikroOptions<ProductVariant>
)) as unknown as T[] )
} }
async findAndCount<T = ProductVariant>( async findAndCount(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<[T[], number]> { ): Promise<[ProductVariant[], 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 findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
}) })
return (await this.manager_.findAndCount( return await manager.findAndCount(
ProductVariant, ProductVariant,
findOptions_.where as MikroFilterQuery<ProductVariant>, findOptions_.where as MikroFilterQuery<ProductVariant>,
findOptions_.options as MikroOptions<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
} }
} }
+59 -33
View File
@@ -1,36 +1,37 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Product } from "@models" import { Product } from "@models"
import { import {
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy, LoadStrategy,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { deduplicateIfNecessary } from "../utils" import {
import { DAL } from "@medusajs/types" 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 protected readonly manager_: SqlEntityManager
constructor({ manager }) {
this.manager_ = manager.fork() constructor({ manager }: { manager: SqlEntityManager }) {
// @ts-ignore
super(...arguments)
this.manager_ = manager
} }
async find( async find(
findOptions: DAL.FindOptions<Product> = { where: {} }, findOptions: DAL.FindOptions<Product> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<Product[]> { ): 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 } const findOptions_ = { ...findOptions }
findOptions_.options ??= {} 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, { Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN, strategy: LoadStrategy.SELECT_IN,
@@ -38,7 +39,7 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
await this.mutateNotInCategoriesConstraints(findOptions_) await this.mutateNotInCategoriesConstraints(findOptions_)
return await this.manager_.find( return await manager.find(
Product, Product,
findOptions_.where as MikroFilterQuery<Product>, findOptions_.where as MikroFilterQuery<Product>,
findOptions_.options as MikroOptions<Product> findOptions_.options as MikroOptions<Product>
@@ -47,20 +48,13 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
async findAndCount( async findAndCount(
findOptions: DAL.FindOptions<Product> = { where: {} }, findOptions: DAL.FindOptions<Product> = { where: {} },
context: { transaction?: any } = {} context: Context = {}
): Promise<[Product[], number]> { ): Promise<[Product[], number]> {
// Spread is used to copy the options in case of manipulation to prevent side effects
const findOptions_ = { ...findOptions } const findOptions_ = { ...findOptions }
findOptions_.options ??= {} findOptions_.options ??= {}
findOptions_.options.limit ??= 15
if (findOptions_.options.populate) { if (context.transactionManager) {
deduplicateIfNecessary(findOptions_.options.populate) Object.assign(findOptions_.options, { ctx: context.transactionManager })
}
if (context.transaction) {
Object.assign(findOptions_.options, { ctx: context.transaction })
} }
Object.assign(findOptions_.options, { Object.assign(findOptions_.options, {
@@ -75,17 +69,20 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
findOptions_.options as MikroOptions<Product> findOptions_.options as MikroOptions<Product>
) )
} }
/** /**
* In order to be able to have a strict not in categories, and prevent a 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 * 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 * first find all products that are in the categories, and then exclude them
*/ */
private async mutateNotInCategoriesConstraints( protected async mutateNotInCategoriesConstraints(
findOptions: DAL.FindOptions<Product> = { where: {} } findOptions: DAL.FindOptions<Product> = { where: {} },
context: Context = {}
): Promise<void> { ): Promise<void> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
if (findOptions.where.categories?.id?.["$nin"]) { if (findOptions.where.categories?.id?.["$nin"]) {
const productsInCategories = await this.manager_.find( const productsInCategories = await manager.find(
Product, Product,
{ {
categories: { 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 { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { import { createConnection } from "../utils"
ProductServiceInitializeCustomDataLayerOptions,
ProductServiceInitializeOptions,
} from "../types"
import { createConnection, loadDatabaseConfig } from "../utils"
import * as ProductModels from "@models" import * as ProductModels from "@models"
import { EntitySchema } from "@mikro-orm/core" 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 * 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, logger,
}: Pick< }: Pick<
LoaderOptions< LoaderOptions<
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>, >,
"options" | "logger" "options" | "logger"
> = {}) { > = {}) {
logger ??= console as unknown as 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 entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(dbData, entities) const orm = await createConnection(dbData, entities)
+6 -9
View File
@@ -1,11 +1,8 @@
import { LoaderOptions, Logger } from "@medusajs/types" import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { import { createConnection } from "../utils"
ProductServiceInitializeCustomDataLayerOptions,
ProductServiceInitializeOptions,
} from "../types"
import { createConnection, loadDatabaseConfig } from "../utils"
import * as ProductModels from "@models" import * as ProductModels from "@models"
import { EntitySchema } from "@mikro-orm/core" 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 * 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, logger,
}: Pick< }: Pick<
LoaderOptions< LoaderOptions<
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>, >,
"options" | "logger" "options" | "logger"
> = {}) { > = {}) {
logger ??= console as unknown as 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 entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(dbData, entities) const orm = await createConnection(dbData, entities)
+6 -9
View File
@@ -1,15 +1,12 @@
import { createConnection, loadDatabaseConfig } from "../utils" import { createConnection } from "../utils"
import * as ProductModels from "@models" import * as ProductModels from "@models"
import { Product, ProductCategory, ProductVariant } from "@models" import { Product, ProductCategory, ProductVariant } from "@models"
import { EntitySchema } from "@mikro-orm/core" import { EntitySchema } from "@mikro-orm/core"
import { LoaderOptions, Logger } from "@medusajs/types" import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import {
ProductServiceInitializeCustomDataLayerOptions,
ProductServiceInitializeOptions,
} from "../types"
import { EOL } from "os" import { EOL } from "os"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { resolve } from "path" import { resolve } from "path"
import { ModulesSdkUtils } from "@medusajs/utils"
export async function run({ export async function run({
options, options,
@@ -18,8 +15,8 @@ export async function run({
}: Partial< }: Partial<
Pick< Pick<
LoaderOptions< LoaderOptions<
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>, >,
"options" | "logger" "options" | "logger"
> >
@@ -38,7 +35,7 @@ export async function run({
logger ??= console as unknown as 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 entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(dbData, entities) 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 { mockContainer, nonExistingProductId } from "../__fixtures__/product"
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),
})
describe("Product service", function () { describe("Product service", function () {
beforeEach(function () { beforeEach(function () {
jest.clearAllMocks() 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 () { it("should list products", async function () {
const productService = container.resolve("productService") const productService = mockContainer.resolve("productService")
const productRepository = container.resolve("productRepository") const productRepository = mockContainer.resolve("productRepository")
const filters = {} const filters = {}
const config = { const config = {
@@ -32,27 +68,31 @@ describe("Product service", function () {
await productService.list(filters, config) await productService.list(filters, config)
expect(productRepository.find).toHaveBeenCalledWith({ expect(productRepository.find).toHaveBeenCalledWith(
{
where: {}, where: {},
options: { options: {
fields: undefined, fields: undefined,
limit: undefined, limit: 15,
offset: undefined, offset: undefined,
populate: [], populate: [],
withDeleted: undefined,
}, },
}) },
undefined
)
}) })
it("should list products with filters", async function () { it("should list products with filters", async function () {
const productService = container.resolve("productService") const productService = mockContainer.resolve("productService")
const productRepository = container.resolve("productRepository") const productRepository = mockContainer.resolve("productRepository")
const filters = { const filters = {
tags: { tags: {
value: { value: {
$in: ["test"], $in: ["test"],
} },
} },
} }
const config = { const config = {
relations: [], relations: [],
@@ -60,33 +100,37 @@ describe("Product service", function () {
await productService.list(filters, config) await productService.list(filters, config)
expect(productRepository.find).toHaveBeenCalledWith({ expect(productRepository.find).toHaveBeenCalledWith(
{
where: { where: {
tags: { tags: {
value: { value: {
$in: ["test"] $in: ["test"],
} },
}, },
}, },
options: { options: {
fields: undefined, fields: undefined,
limit: undefined, limit: 15,
offset: undefined, offset: undefined,
populate: [], populate: [],
withDeleted: undefined,
}, },
}) },
undefined
)
}) })
it("should list products with filters and relations", async function () { it("should list products with filters and relations", async function () {
const productService = container.resolve("productService") const productService = mockContainer.resolve("productService")
const productRepository = container.resolve("productRepository") const productRepository = mockContainer.resolve("productRepository")
const filters = { const filters = {
tags: { tags: {
value: { value: {
$in: ["test"], $in: ["test"],
} },
} },
} }
const config = { const config = {
relations: ["tags"], relations: ["tags"],
@@ -94,20 +138,62 @@ describe("Product service", function () {
await productService.list(filters, config) await productService.list(filters, config)
expect(productRepository.find).toHaveBeenCalledWith({ expect(productRepository.find).toHaveBeenCalledWith(
{
where: { where: {
tags: { tags: {
value: { value: {
$in: ["test"] $in: ["test"],
} },
}, },
}, },
options: { options: {
fields: undefined, fields: undefined,
limit: undefined, limit: 15,
offset: undefined, offset: undefined,
withDeleted: undefined,
populate: ["tags"], 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
)
}) })
}) })
+3
View File
@@ -4,3 +4,6 @@ export { default as ProductTagService } from "./product-tag"
export { default as ProductVariantService } from "./product-variant" export { default as ProductVariantService } from "./product-variant"
export { default as ProductCollectionService } from "./product-collection" export { default as ProductCollectionService } from "./product-collection"
export { default as ProductCategoryService } from "./product-category" 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 { ProductCategory } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { buildQuery } from "../utils" import { ModulesSdkUtils, MedusaError, isDefined } from "@medusajs/utils"
type InjectedDependencies = { type InjectedDependencies = {
productCategoryRepository: DAL.RepositoryService productCategoryRepository: DAL.TreeRepositoryService
} }
export default class ProductCategoryService<TEntity = ProductCategory> { export default class ProductCategoryService<
protected readonly productCategoryRepository_: DAL.RepositoryService TEntity extends ProductCategory = ProductCategory
> {
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
constructor({ productCategoryRepository }: InjectedDependencies) { constructor({ productCategoryRepository }: InjectedDependencies) {
this.productCategoryRepository_ = productCategoryRepository 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( async list(
filters: ProductTypes.FilterableProductCategoryProps = {}, filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {}, config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<TEntity[]> { ): Promise<TEntity[]> {
const transformOptions = { const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false includeDescendantsTree: filters?.include_descendants_tree || false,
} }
delete filters.include_descendants_tree delete filters.include_descendants_tree
const queryOptions = buildQuery<TEntity>(filters, config) const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
queryOptions.where ??= {} queryOptions.where ??= {}
return await this.productCategoryRepository_.find( return (await this.productCategoryRepository_.find(
queryOptions, queryOptions,
transformOptions, 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 { ProductCollection } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { buildQuery } from "../utils" import { ModulesSdkUtils, retrieveEntity } from "@medusajs/utils"
type InjectedDependencies = { type InjectedDependencies = {
productCollectionRepository: DAL.RepositoryService productCollectionRepository: DAL.RepositoryService
} }
export default class ProductCollectionService<TEntity = ProductCollection> { export default class ProductCollectionService<
protected readonly productCollectionRepository_: DAL.RepositoryService<TEntity> TEntity extends ProductCollection = ProductCollection
> {
protected readonly productCollectionRepository_: DAL.TreeRepositoryService
constructor({ productCollectionRepository }: InjectedDependencies) { constructor({ productCollectionRepository }: InjectedDependencies) {
this.productCollectionRepository_ = productCollectionRepository 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( async list(
filters: ProductTypes.FilterableProductCollectionProps = {}, filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {}, config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<TEntity[]> { ): 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 ??= {} queryOptions.where ??= {}
if (filters.title) { if (filters.title) {
queryOptions.where["title"] = { $like: 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 { import {
ProductCategoryService, ProductCategoryService,
ProductCollectionService, ProductCollectionService,
ProductOptionService,
ProductService, ProductService,
ProductTagService, ProductTagService,
ProductTypeService,
ProductVariantService, ProductVariantService,
} from "@services" } from "@services"
import { import {
Image,
Product, Product,
ProductCategory, ProductCategory,
ProductCollection, ProductCollection,
ProductOption,
ProductTag, ProductTag,
ProductType,
ProductVariant, ProductVariant,
} from "@models" } 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 = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService
productService: ProductService<any> productService: ProductService<any>
productVariantService: ProductVariantService<any> productVariantService: ProductVariantService<any, any>
productTagService: ProductTagService<any> productTagService: ProductTagService<any>
productCategoryService: ProductCategoryService<any> productCategoryService: ProductCategoryService<any>
productCollectionService: ProductCollectionService<any> productCollectionService: ProductCollectionService<any>
productImageService: ProductImageService<any>
productTypeService: ProductTypeService<any>
productOptionService: ProductOptionService<any>
} }
export default class ProductModuleService< export default class ProductModuleService<
TProduct = Product, TProduct extends Product = Product,
TProductVariant = ProductVariant, TProductVariant extends ProductVariant = ProductVariant,
TProductTag = ProductTag, TProductTag extends ProductTag = ProductTag,
TProductCollection = ProductCollection, TProductCollection extends ProductCollection = ProductCollection,
TProductCategory = ProductCategory TProductCategory extends ProductCategory = ProductCategory,
> implements TProductImage extends Image = Image,
ProductTypes.IProductModuleService< TProductType extends ProductType = ProductType,
TProduct, TProductOption extends ProductOption = ProductOption
TProductVariant, > implements ProductTypes.IProductModuleService
TProductTag,
TProductCollection,
TProductCategory
>
{ {
protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ProductService<TProduct> protected readonly productService_: ProductService<TProduct>
protected readonly productVariantService: ProductVariantService<TProductVariant> protected readonly productVariantService_: ProductVariantService<
protected readonly productCategoryService: ProductCategoryService<TProductCategory> TProductVariant,
protected readonly productTagService: ProductTagService<TProductTag> TProduct
protected readonly productCollectionService: ProductCollectionService<TProductCollection> >
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({ constructor(
{
baseRepository,
productService, productService,
productVariantService, productVariantService,
productTagService, productTagService,
productCategoryService, productCategoryService,
productCollectionService, productCollectionService,
}: InjectedDependencies) { productImageService,
productTypeService,
productOptionService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.productService_ = productService this.productService_ = productService
this.productVariantService = productVariantService this.productVariantService_ = productVariantService
this.productTagService = productTagService this.productTagService_ = productTagService
this.productCategoryService = productCategoryService this.productCategoryService_ = productCategoryService
this.productCollectionService = productCollectionService this.productCollectionService_ = productCollectionService
this.productImageService_ = productImageService
this.productTypeService_ = productTypeService
this.productOptionService_ = productOptionService
} }
async list( async list(
filters: ProductTypes.FilterableProductProps = {}, filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {}, config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductTypes.ProductDTO[]> { ): Promise<ProductTypes.ProductDTO[]> {
const products = await this.productService_.list( const products = await this.productService_.list(
filters, filters,
@@ -71,10 +110,22 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(products)) 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( async listAndCount(
filters: ProductTypes.FilterableProductProps = {}, filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {}, config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<[ProductTypes.ProductDTO[], number]> { ): Promise<[ProductTypes.ProductDTO[], number]> {
const [products, count] = await this.productService_.listAndCount( const [products, count] = await this.productService_.listAndCount(
filters, filters,
@@ -85,12 +136,26 @@ export default class ProductModuleService<
return [JSON.parse(JSON.stringify(products)), count] 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( async listVariants(
filters: ProductTypes.FilterableProductVariantProps = {}, filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {}, config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductTypes.ProductVariantDTO[]> { ): Promise<ProductTypes.ProductVariantDTO[]> {
const variants = await this.productVariantService.list( const variants = await this.productVariantService_.list(
filters, filters,
config, config,
sharedContext sharedContext
@@ -99,12 +164,26 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(variants)) 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( async listTags(
filters: ProductTypes.FilterableProductTagProps = {}, filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {}, config: FindConfig<ProductTypes.ProductTagDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO[]> { ): Promise<ProductTypes.ProductTagDTO[]> {
const tags = await this.productTagService.list( const tags = await this.productTagService_.list(
filters, filters,
config, config,
sharedContext sharedContext
@@ -113,12 +192,26 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(tags)) 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( async listCollections(
filters: ProductTypes.FilterableProductCollectionProps = {}, filters: ProductTypes.FilterableProductCollectionProps = {},
config: FindConfig<ProductTypes.ProductCollectionDTO> = {}, config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductTypes.ProductCollectionDTO[]> { ): Promise<ProductTypes.ProductCollectionDTO[]> {
const collections = await this.productCollectionService.list( const collections = await this.productCollectionService_.list(
filters, filters,
config, config,
sharedContext sharedContext
@@ -127,12 +220,40 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(collections)) 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( async listCategories(
filters: ProductTypes.FilterableProductCategoryProps = {}, filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {}, config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO[]> { ): Promise<ProductTypes.ProductCategoryDTO[]> {
const categories = await this.productCategoryService.list( const categories = await this.productCategoryService_.list(
filters, filters,
config, config,
sharedContext sharedContext
@@ -140,4 +261,199 @@ export default class ProductModuleService<
return JSON.parse(JSON.stringify(categories)) 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[]
}
}
+35 -7
View File
@@ -1,13 +1,27 @@
import { ProductTag } from "@models" import { ProductTag } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" import {
import { buildQuery } from "../utils" 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 = { type InjectedDependencies = {
productTagRepository: DAL.RepositoryService productTagRepository: DAL.RepositoryService
} }
export default class ProductTagService<TEntity = ProductTag> { export default class ProductTagService<
protected readonly productTagRepository_: DAL.RepositoryService<TEntity> TEntity extends ProductTag = ProductTag
> {
protected readonly productTagRepository_: DAL.RepositoryService
constructor({ productTagRepository }: InjectedDependencies) { constructor({ productTagRepository }: InjectedDependencies) {
this.productTagRepository_ = productTagRepository this.productTagRepository_ = productTagRepository
@@ -16,14 +30,28 @@ export default class ProductTagService<TEntity = ProductTag> {
async list( async list(
filters: ProductTypes.FilterableProductTagProps = {}, filters: ProductTypes.FilterableProductTagProps = {},
config: FindConfig<ProductTypes.ProductTagDTO> = {}, config: FindConfig<ProductTypes.ProductTagDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<TEntity[]> { ): Promise<TEntity[]> {
const queryOptions = buildQuery<TEntity>(filters, config) const queryOptions = ModulesSdkUtils.buildQuery<ProductTag>(filters, config)
if (filters.value) { if (filters.value) {
queryOptions.where["value"] = { $ilike: 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 { Product, ProductVariant } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
import { buildQuery } from "../utils" import {
InjectTransactionManager,
isString,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import ProductService from "./product"
import { doNotForceTransaction } from "../utils"
import { ProductVariantRepository } from "@repositories"
type InjectedDependencies = { type InjectedDependencies = {
productVariantRepository: DAL.RepositoryService productVariantRepository: DAL.RepositoryService
productService: ProductService<any>
} }
export default class ProductVariantService<TEntity = ProductVariant> { export default class ProductVariantService<
protected readonly productVariantRepository_: DAL.RepositoryService<TEntity> 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.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( async list(
filters: ProductTypes.FilterableProductVariantProps = {}, filters: ProductTypes.FilterableProductVariantProps = {},
config: FindConfig<ProductTypes.ProductVariantDTO> = {}, config: FindConfig<ProductTypes.ProductVariantDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<TEntity[]> { ): Promise<TEntity[]> {
const queryOptions = buildQuery<TEntity>(filters, config) const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
return await this.productVariantRepository_.find(queryOptions) 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[]
} }
} }
+99 -13
View File
@@ -1,25 +1,55 @@
import { ProductTagService, ProductVariantService } from "@services"
import { Product } from "@models" import { Product } from "@models"
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" import {
import { buildQuery } from "../utils" 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 = { type InjectedDependencies = {
productRepository: DAL.RepositoryService productRepository: DAL.RepositoryService
productVariantService: ProductVariantService
productTagService: ProductTagService
} }
export default class ProductService<TEntity = Product> { export default class ProductService<TEntity extends Product = Product> {
protected readonly productRepository_: DAL.RepositoryService<TEntity> protected readonly productRepository_: DAL.RepositoryService
constructor({ productRepository }: InjectedDependencies) { constructor({ productRepository }: InjectedDependencies) {
this.productRepository_ = productRepository 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( async list(
filters: ProductTypes.FilterableProductProps = {}, filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {}, config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<TEntity[]> { ): Promise<TEntity[]> {
if (filters.category_ids) { if (filters.category_ids) {
if (Array.isArray(filters.category_ids)) { if (Array.isArray(filters.category_ids)) {
@@ -34,14 +64,17 @@ export default class ProductService<TEntity = Product> {
delete filters.category_ids delete filters.category_ids
} }
const queryOptions = buildQuery<TEntity>(filters, config) const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
return await this.productRepository_.find(queryOptions) return (await this.productRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
} }
async listAndCount( async listAndCount(
filters: ProductTypes.FilterableProductProps = {}, filters: ProductTypes.FilterableProductProps = {},
config: FindConfig<ProductTypes.ProductDTO> = {}, config: FindConfig<ProductTypes.ProductDTO> = {},
sharedContext?: SharedContext sharedContext?: Context
): Promise<[TEntity[], number]> { ): Promise<[TEntity[], number]> {
if (filters.category_ids) { if (filters.category_ids) {
if (Array.isArray(filters.category_ids)) { if (Array.isArray(filters.category_ids)) {
@@ -56,7 +89,60 @@ export default class ProductService<TEntity = Product> {
delete filters.category_ids delete filters.category_ids
} }
const queryOptions = buildQuery<TEntity>(filters, config) const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
return await this.productRepository_.findAndCount(queryOptions) 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 -14
View File
@@ -1,17 +1,4 @@
import { Constructor, DAL, IEventBusService } from "@medusajs/types" import { 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> }
}
export type InitializeModuleInjectableDependencies = { export type InitializeModuleInjectableDependencies = {
eventBusService?: IEventBusService eventBusService?: IEventBusService
@@ -1,15 +1,15 @@
import { MikroORM, PostgreSqlDriver } from "@mikro-orm/postgresql" import { MikroORM, PostgreSqlDriver } from "@mikro-orm/postgresql"
import { ProductServiceInitializeOptions } from "../types" import { ModuleServiceInitializeOptions } from "@medusajs/types"
export async function createConnection( export async function createConnection(
database: ProductServiceInitializeOptions["database"], database: ModuleServiceInitializeOptions["database"],
entities: any[] entities: any[]
) { ) {
const schema = database.schema || "public" const schema = database.schema || "public"
const orm = await MikroORM.init<PostgreSqlDriver>({ const orm = await MikroORM.init<PostgreSqlDriver>({
discovery: { disableDynamicFileAccess: true }, discovery: { disableDynamicFileAccess: true },
entities, entities,
debug: process.env.NODE_ENV === "development", debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false,
baseDir: process.cwd(), baseDir: process.cwd(),
clientUrl: database.clientUrl, clientUrl: database.clientUrl,
schema, schema,
+11 -2
View File
@@ -1,3 +1,12 @@
export * from "./query" import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
export * from "./create-connection" 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,
})
}
+1
View File
@@ -3,6 +3,7 @@
"version": "1.8.10", "version": "1.8.10",
"description": "Medusa Types definition", "description": "Medusa Types definition",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/medusajs/medusa", "url": "https://github.com/medusajs/medusa",
+2 -1
View File
@@ -41,11 +41,12 @@ export type Writable<T> = {
} }
export interface FindConfig<Entity> { export interface FindConfig<Entity> {
select?: (keyof Entity)[] select?: (keyof Entity | string)[]
skip?: number skip?: number
take?: number take?: number
relations?: string[] relations?: string[]
order?: { [K: string]: "ASC" | "DESC" } order?: { [K: string]: "ASC" | "DESC" }
withDeleted?: boolean
} }
export type ExtendedFindConfig<TEntity> = ( export type ExtendedFindConfig<TEntity> = (
+2
View File
@@ -21,4 +21,6 @@ export type FindOptions<T = any> = {
options?: OptionsQuery<T, any> options?: OptionsQuery<T, any>
} }
export const SoftDeletableFilterKey = "softDeletable"
export * from "./repository-service" export * from "./repository-service"
+51 -2
View File
@@ -1,5 +1,6 @@
import { FindOptions } from "./index" import { FindOptions } from "./index"
import { RepositoryTransformOptions } from "../common" import { RepositoryTransformOptions } from "../common"
import { Context } from "../shared-context"
/** /**
* Data access layer (DAL) interface to implements for any repository service. * 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. * ORM directly and allows to switch to another ORM without changing the business logic.
*/ */
export interface RepositoryService<T = any> { export interface RepositoryService<T = any> {
find(options?: FindOptions<T>, transformOptions?: RepositoryTransformOptions): Promise<T[]> transaction(
findAndCount(options?: FindOptions<T>, transformOptions?: RepositoryTransformOptions): Promise<[T[], number]> 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 context?: SharedContext
): Promise<ReservationItemDTO> ): Promise<ReservationItemDTO>
// TODO make it bulk
createInventoryItem( createInventoryItem(
input: CreateInventoryItemInput, input: CreateInventoryItemInput,
context?: SharedContext context?: SharedContext
@@ -95,6 +96,7 @@ export interface IInventoryService {
context?: SharedContext context?: SharedContext
): Promise<void> ): Promise<void>
// TODO make it bulk
deleteInventoryItem( deleteInventoryItem(
inventoryItemId: string, inventoryItemId: string,
context?: SharedContext context?: SharedContext
+15
View File
@@ -1,5 +1,6 @@
import { MedusaContainer } from "../common" import { MedusaContainer } from "../common"
import { Logger } from "../logger" import { Logger } from "../logger"
import { RepositoryService } from "../dal"
export type Constructor<T> = new (...args: any[]) => T export type Constructor<T> = new (...args: any[]) => T
export * from "../common/medusa-container" export * from "../common/medusa-container"
@@ -93,3 +94,17 @@ export type ModuleExports = {
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration
): Promise<void> ): 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> }
}
+133 -1
View File
@@ -34,6 +34,7 @@ export interface ProductDTO {
tags: ProductTagDTO[] tags: ProductTagDTO[]
variants: ProductVariantDTO[] variants: ProductVariantDTO[]
options: ProductOptionDTO[] options: ProductOptionDTO[]
images: ProductImageDTO[]
discountable?: boolean discountable?: boolean
external_id?: string | null external_id?: string | null
created_at?: string | Date created_at?: string | Date
@@ -86,7 +87,7 @@ export interface ProductTagDTO {
id: string id: string
value: string value: string
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
products: ProductDTO[] products?: ProductDTO[]
} }
export interface ProductCollectionDTO { export interface ProductCollectionDTO {
@@ -95,6 +96,7 @@ export interface ProductCollectionDTO {
handle: string handle: string
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
deleted_at?: string | Date deleted_at?: string | Date
products?: ProductDTO[]
} }
export interface ProductTypeDTO { export interface ProductTypeDTO {
@@ -113,6 +115,13 @@ export interface ProductOptionDTO {
deleted_at?: string | Date deleted_at?: string | Date
} }
export interface ProductImageDTO {
id: string
url: string
metadata?: Record<string, unknown> | null
deleted_at?: string | Date
}
export interface ProductOptionValueDTO { export interface ProductOptionValueDTO {
id: string id: string
value: string value: string
@@ -164,3 +173,126 @@ export interface FilterableProductCategoryProps
is_internal?: boolean is_internal?: boolean
include_descendants_tree?: 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
}
+61 -14
View File
@@ -1,4 +1,5 @@
import { import {
CreateProductDTO,
FilterableProductCategoryProps, FilterableProductCategoryProps,
FilterableProductCollectionProps, FilterableProductCollectionProps,
FilterableProductProps, FilterableProductProps,
@@ -11,48 +12,94 @@ import {
ProductVariantDTO, ProductVariantDTO,
} from "./common" } from "./common"
import { FindConfig } 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( list(
filters?: FilterableProductProps, filters?: FilterableProductProps,
config?: FindConfig<ProductDTO>, config?: FindConfig<ProductDTO>,
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductDTO[]> ): Promise<ProductDTO[]>
listAndCount( listAndCount(
filters?: FilterableProductProps, filters?: FilterableProductProps,
config?: FindConfig<ProductDTO>, config?: FindConfig<ProductDTO>,
sharedContext?: SharedContext sharedContext?: Context
): Promise<[ProductDTO[], number]> ): Promise<[ProductDTO[], number]>
listTags( listTags(
filters?: FilterableProductTagProps, filters?: FilterableProductTagProps,
config?: FindConfig<ProductTagDTO>, config?: FindConfig<ProductTagDTO>,
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductTagDTO[]> ): Promise<ProductTagDTO[]>
retrieveVariant(
productVariantId: string,
config?: FindConfig<ProductVariantDTO>,
sharedContext?: Context
): Promise<ProductVariantDTO>
listVariants( listVariants(
filters?: FilterableProductVariantProps, filters?: FilterableProductVariantProps,
config?: FindConfig<ProductVariantDTO>, config?: FindConfig<ProductVariantDTO>,
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductVariantDTO[]> ): Promise<ProductVariantDTO[]>
listAndCountVariants(
filters?: FilterableProductVariantProps,
config?: FindConfig<ProductVariantDTO>,
sharedContext?: Context
): Promise<[ProductVariantDTO[], number]>
retrieveCollection(
productCollectionId: string,
config?: FindConfig<ProductCollectionDTO>,
sharedContext?: Context
): Promise<ProductCollectionDTO>
listCollections( listCollections(
filters?: FilterableProductCollectionProps, filters?: FilterableProductCollectionProps,
config?: FindConfig<ProductCollectionDTO>, config?: FindConfig<ProductCollectionDTO>,
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductCollectionDTO[]> ): Promise<ProductCollectionDTO[]>
listAndCountCollections(
filters?: FilterableProductCollectionProps,
config?: FindConfig<ProductCollectionDTO>,
sharedContext?: Context
): Promise<[ProductCollectionDTO[], number]>
retrieveCategory(
productCategoryId: string,
config?: FindConfig<ProductCategoryDTO>,
sharedContext?: Context
): Promise<ProductCategoryDTO>
listCategories( listCategories(
filters?: FilterableProductCategoryProps, filters?: FilterableProductCategoryProps,
config?: FindConfig<ProductCategoryDTO>, config?: FindConfig<ProductCategoryDTO>,
sharedContext?: SharedContext sharedContext?: Context
): Promise<ProductCategoryDTO[]> ): 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[]>
} }
+6
View File
@@ -3,3 +3,9 @@ import { EntityManager } from "typeorm"
export type SharedContext = { export type SharedContext = {
transactionManager?: EntityManager transactionManager?: EntityManager
} }
export type Context<TManager = unknown> = {
transactionManager?: TManager
isolationLevel?: string
enableNestedTransactions?: boolean
}
+3 -1
View File
@@ -3,6 +3,7 @@
"version": "1.9.2", "version": "1.9.2",
"description": "Medusa utilities functions shared by Medusa core and Modules", "description": "Medusa utilities functions shared by Medusa core and Modules",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/medusajs/medusa", "url": "https://github.com/medusajs/medusa",
@@ -22,6 +23,7 @@
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"express": "^4.18.2", "express": "^4.18.2",
"jest": "^25.5.4", "jest": "^25.5.4",
"rimraf": "^5.0.1",
"ts-jest": "^25.5.1", "ts-jest": "^25.5.1",
"typescript": "^4.4.4" "typescript": "^4.4.4"
}, },
@@ -32,7 +34,7 @@
}, },
"scripts": { "scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build", "prepare": "cross-env NODE_ENV=production yarn run build",
"build": "tsc --build", "build": "rimraf dist && tsc --build",
"watch": "tsc --build --watch", "watch": "tsc --build --watch",
"test": "jest" "test": "jest"
} }
+4 -4
View File
@@ -1,4 +1,4 @@
export * as DecoratorUtils from "./decorators"; export * as DecoratorUtils from "./decorators"
export * as EventBusUtils from "./event-bus"; export * as EventBusUtils from "./event-bus"
export * as SearchUtils from "./search"; export * as SearchUtils from "./search"
export * as ModulesSdkUtils from "./modules-sdk"
+3
View File
@@ -0,0 +1,3 @@
export function deduplicate<T = any>(collection: T[]): T[] {
return [...new Set(collection)]
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./build-query" export * from "./build-query"
export * from "./deduplicate"
export * from "./errors" export * from "./errors"
export * from "./generate-entity-id" export * from "./generate-entity-id"
export * from "./get-config-file" export * from "./get-config-file"
@@ -4,16 +4,7 @@ export function MedusaContext() {
propertyKey: string | symbol, propertyKey: string | symbol,
parameterIndex: number parameterIndex: number
) { ) {
if (!target.MedusaContextIndex_) { target.MedusaContextIndex_ ??= {}
target.MedusaContextIndex_ = {}
}
if (propertyKey in target.MedusaContextIndex_) {
throw new Error(
`Only one MedusaContext is allowed on method "${String(propertyKey)}".`
)
}
target.MedusaContextIndex_[propertyKey] = parameterIndex target.MedusaContextIndex_[propertyKey] = parameterIndex
} }
} }
@@ -1,8 +1,8 @@
import { SharedContext } from "@medusajs/types" import { Context, SharedContext } from "@medusajs/types"
export function InjectEntityManager( export function InjectEntityManager(
shouldForceTransaction: (target: any) => boolean = () => false, shouldForceTransaction: (target: any) => boolean = () => false,
managerProperty: string = "manager_" managerProperty: string | false = "manager_"
): MethodDecorator { ): MethodDecorator {
return function ( return function (
target: any, target: any,
@@ -20,18 +20,27 @@ export function InjectEntityManager(
const argIndex = target.MedusaContextIndex_[propertyKey] const argIndex = target.MedusaContextIndex_[propertyKey]
descriptor.value = async function (...args: any[]) { descriptor.value = async function (...args: any[]) {
const shouldForceTransactionRes = shouldForceTransaction(target) const shouldForceTransactionRes = shouldForceTransaction(target)
const context: SharedContext = args[argIndex] ?? {} const context: SharedContext | Context = args[argIndex] ?? {}
if (!shouldForceTransactionRes && context?.transactionManager) { if (!shouldForceTransactionRes && context?.transactionManager) {
return await originalMethod.apply(this, args) return await originalMethod.apply(this, args)
} }
return await this[managerProperty].transaction( return await (managerProperty === false
? this
: this[managerProperty]
).transaction(
async (transactionManager) => { async (transactionManager) => {
args[argIndex] = args[argIndex] ?? {} args[argIndex] = args[argIndex] ?? {}
args[argIndex].transactionManager = transactionManager args[argIndex].transactionManager = transactionManager
return await originalMethod.apply(this, args) return await originalMethod.apply(this, args)
},
{
transaction: context?.transactionManager,
isolationLevel: (context as Context)?.isolationLevel,
enableNestedTransactions:
(context as Context).enableNestedTransactions ?? false,
} }
) )
} }
+1
View File
@@ -4,3 +4,4 @@ export * from "./common"
export * from "./decorators" export * from "./decorators"
export * from "./event-bus" export * from "./event-bus"
export * from "./search" export * from "./search"
export * from "./modules-sdk"
@@ -1,4 +1,4 @@
import { loadDatabaseConfig } from "../load-database-config" import { loadDatabaseConfig } from "../load-module-database-config"
describe("loadDatabaseConfig", function () { describe("loadDatabaseConfig", function () {
afterEach(() => { afterEach(() => {
@@ -8,7 +8,7 @@ describe("loadDatabaseConfig", function () {
it("should return the local configuration using the environment variable", function () { it("should return the local configuration using the environment variable", function () {
process.env.POSTGRES_URL = "postgres://localhost:5432/medusa" process.env.POSTGRES_URL = "postgres://localhost:5432/medusa"
let config = loadDatabaseConfig() let config = loadDatabaseConfig("product")
expect(config).toEqual({ expect(config).toEqual({
clientUrl: process.env.POSTGRES_URL, clientUrl: process.env.POSTGRES_URL,
@@ -17,12 +17,13 @@ describe("loadDatabaseConfig", function () {
ssl: false, ssl: false,
}, },
}, },
debug: false,
schema: "", schema: "",
}) })
delete process.env.POSTGRES_URL delete process.env.POSTGRES_URL
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa" process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa"
config = loadDatabaseConfig() config = loadDatabaseConfig("product")
expect(config).toEqual({ expect(config).toEqual({
clientUrl: process.env.PRODUCT_POSTGRES_URL, clientUrl: process.env.PRODUCT_POSTGRES_URL,
@@ -31,13 +32,14 @@ describe("loadDatabaseConfig", function () {
ssl: false, ssl: false,
}, },
}, },
debug: false,
schema: "", schema: "",
}) })
}) })
it("should return the remote configuration using the environment variable", function () { it("should return the remote configuration using the environment variable", function () {
process.env.POSTGRES_URL = "postgres://https://test.com:5432/medusa" process.env.POSTGRES_URL = "postgres://https://test.com:5432/medusa"
let config = loadDatabaseConfig() let config = loadDatabaseConfig("product")
expect(config).toEqual({ expect(config).toEqual({
clientUrl: process.env.POSTGRES_URL, clientUrl: process.env.POSTGRES_URL,
@@ -48,12 +50,13 @@ describe("loadDatabaseConfig", function () {
}, },
}, },
}, },
debug: false,
schema: "", schema: "",
}) })
delete process.env.POSTGRES_URL delete process.env.POSTGRES_URL
process.env.PRODUCT_POSTGRES_URL = "postgres://https://test.com:5432/medusa" process.env.PRODUCT_POSTGRES_URL = "postgres://https://test.com:5432/medusa"
config = loadDatabaseConfig() config = loadDatabaseConfig("product")
expect(config).toEqual({ expect(config).toEqual({
clientUrl: process.env.PRODUCT_POSTGRES_URL, clientUrl: process.env.PRODUCT_POSTGRES_URL,
@@ -64,6 +67,7 @@ describe("loadDatabaseConfig", function () {
}, },
}, },
}, },
debug: false,
schema: "", schema: "",
}) })
}) })
@@ -76,7 +80,7 @@ describe("loadDatabaseConfig", function () {
}, },
} }
const config = loadDatabaseConfig(options) let config = loadDatabaseConfig("product", options)
expect(config).toEqual({ expect(config).toEqual({
clientUrl: options.database.clientUrl, clientUrl: options.database.clientUrl,
@@ -85,6 +89,7 @@ describe("loadDatabaseConfig", function () {
ssl: false, ssl: false,
}, },
}, },
debug: false,
schema: "", schema: "",
}) })
}) })
@@ -97,7 +102,7 @@ describe("loadDatabaseConfig", function () {
}, },
} }
const config = loadDatabaseConfig(options) let config = loadDatabaseConfig("product", options)
expect(config).toEqual({ expect(config).toEqual({
clientUrl: options.database.clientUrl, clientUrl: options.database.clientUrl,
@@ -108,6 +113,7 @@ describe("loadDatabaseConfig", function () {
}, },
}, },
}, },
debug: false,
schema: "", schema: "",
}) })
}) })
@@ -115,7 +121,7 @@ describe("loadDatabaseConfig", function () {
it("should throw if no clientUrl is provided", function () { it("should throw if no clientUrl is provided", function () {
let error let error
try { try {
loadDatabaseConfig() loadDatabaseConfig("product")
} catch (e) { } catch (e) {
error = e error = e
} }
@@ -1,12 +1,5 @@
/** import { DAL, FindConfig, SoftDeletableFilterKey } from "@medusajs/types"
* Move to a new build query utils import { deduplicate, isObject } from "../common"
*/
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
}
export function buildQuery<T = any, TDto = any>( export function buildQuery<T = any, TDto = any>(
filters: Record<string, any> = {}, filters: Record<string, any> = {},
@@ -16,11 +9,18 @@ export function buildQuery<T = any, TDto = any>(
buildWhere(filters, where) buildWhere(filters, where)
const findOptions: DAL.OptionsQuery<T, any> = { const findOptions: DAL.OptionsQuery<T, any> = {
populate: config.relations ?? [], populate: deduplicate(config.relations ?? []),
fields: config.select, fields: config.select as string[],
limit: config.take, limit: config.take ?? 15,
offset: config.skip, offset: config.skip,
} as any }
if (config.withDeleted) {
findOptions.filters ??= {}
findOptions.filters[SoftDeletableFilterKey] = {
withDeleted: true,
}
}
return { where, options: findOptions } return { where, options: findOptions }
} }
@@ -28,7 +28,7 @@ export function buildQuery<T = any, TDto = any>(
function buildWhere(filters: Record<string, any> = {}, where = {}) { function buildWhere(filters: Record<string, any> = {}, where = {}) {
for (let [prop, value] of Object.entries(filters)) { for (let [prop, value] of Object.entries(filters)) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
value = deduplicateIfNecessary(value) value = deduplicate(value)
where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value } where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value }
continue 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,
}
)
}
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./load-module-database-config"
export * from "./decorators"
export * from "./build-query"
export * from "./retrieve-entity"
@@ -1,23 +1,19 @@
import { import { MedusaError } from "../common"
ProductServiceInitializeCustomDataLayerOptions, import { ModulesSdkTypes } from "@medusajs/types"
ProductServiceInitializeOptions,
} from "../types"
import { MedusaError } from "@medusajs/utils"
function getEnv(key: string): string { function getEnv(key: string, moduleName: string): string {
const value = process.env[`PRODUCT_${key}`] ?? process.env[`${key}`] const value =
process.env[`${moduleName.toUpperCase()}_${key}`] ?? process.env[`${key}`]
return value ?? "" return value ?? ""
} }
function isProductServiceInitializeOptions( function isModuleServiceInitializeOptions(
obj: unknown obj: unknown
): obj is ProductServiceInitializeOptions { ): obj is ModulesSdkTypes.ModuleServiceInitializeOptions {
return !!(obj as ProductServiceInitializeOptions)?.database return !!(obj as any)?.database
} }
function getDefaultDriverOptions( function getDefaultDriverOptions(clientUrl: string) {
clientUrl: string
): ProductServiceInitializeOptions["database"]["driverOptions"] {
const localOptions = { const localOptions = {
connection: { connection: {
ssl: false, ssl: false,
@@ -45,31 +41,35 @@ function getDefaultDriverOptions(
/** /**
* Load the config for the database connection. The options can be retrieved * 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 options
* @param moduleName
*/ */
export function loadDatabaseConfig( export function loadDatabaseConfig(
moduleName: string,
options?: options?:
| ProductServiceInitializeOptions | ModulesSdkTypes.ModuleServiceInitializeOptions
| ProductServiceInitializeCustomDataLayerOptions | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
): ProductServiceInitializeOptions["database"] { ): ModulesSdkTypes.ModuleServiceInitializeOptions["database"] {
const clientUrl = getEnv("POSTGRES_URL") const clientUrl = getEnv("POSTGRES_URL", moduleName)
const database: ProductServiceInitializeOptions["database"] = { const database = {
clientUrl: getEnv("POSTGRES_URL"), clientUrl: getEnv("POSTGRES_URL", moduleName),
schema: getEnv("POSTGRES_SCHEMA") ?? "public", schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public",
driverOptions: JSON.parse( driverOptions: JSON.parse(
getEnv("POSTGRES_DRIVER_OPTIONS") || getEnv("POSTGRES_DRIVER_OPTIONS", moduleName) ||
JSON.stringify(getDefaultDriverOptions(clientUrl)) 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.clientUrl = options.database.clientUrl ?? database.clientUrl
database.schema = options.database.schema ?? database.schema database.schema = options.database.schema ?? database.schema
database.driverOptions = database.driverOptions =
options.database.driverOptions ?? options.database.driverOptions ??
getDefaultDriverOptions(database.clientUrl) getDefaultDriverOptions(database.clientUrl)
database.debug = options.database.debug ?? database.debug
} }
if (!database.clientUrl) { 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]
}
+149 -19
View File
@@ -4266,6 +4266,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
dependencies:
string-width: ^5.1.2
string-width-cjs: "npm:string-width@^4.2.0"
strip-ansi: ^7.0.1
strip-ansi-cjs: "npm:strip-ansi@^6.0.1"
wrap-ansi: ^8.1.0
wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0"
checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e
languageName: node
linkType: hard
"@istanbuljs/load-nyc-config@npm:^1.0.0": "@istanbuljs/load-nyc-config@npm:^1.0.0":
version: 1.1.0 version: 1.1.0
resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" resolution: "@istanbuljs/load-nyc-config@npm:1.1.0"
@@ -6486,6 +6500,7 @@ __metadata:
awilix: ^8.0.0 awilix: ^8.0.0
cross-env: ^5.2.1 cross-env: ^5.2.1
dotenv: ^16.1.4 dotenv: ^16.1.4
faker: ^6.6.6
jest: ^25.5.4 jest: ^25.5.4
lodash: ^4.17.21 lodash: ^4.17.21
medusa-test-utils: ^1.1.40 medusa-test-utils: ^1.1.40
@@ -6542,6 +6557,7 @@ __metadata:
express: ^4.18.2 express: ^4.18.2
glob: ^7.1.6 glob: ^7.1.6
jest: ^25.5.4 jest: ^25.5.4
rimraf: ^5.0.1
ts-jest: ^25.5.1 ts-jest: ^25.5.1
typescript: ^4.4.4 typescript: ^4.4.4
ulid: ^2.3.0 ulid: ^2.3.0
@@ -7607,6 +7623,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd
languageName: node
linkType: hard
"@pmmmwh/react-refresh-webpack-plugin@npm:^0.4.3": "@pmmmwh/react-refresh-webpack-plugin@npm:^0.4.3":
version: 0.4.3 version: 0.4.3
resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.4.3" resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.4.3"
@@ -13995,7 +14018,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ansi-styles@npm:^6.0.0": "ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0":
version: 6.2.1 version: 6.2.1
resolution: "ansi-styles@npm:6.2.1" resolution: "ansi-styles@npm:6.2.1"
checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c
@@ -21586,6 +21609,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"faker@npm:^6.6.6":
version: 6.6.6
resolution: "faker@npm:6.6.6"
checksum: 21f6a57adc921ffb61e5ff767024a699a414047264ff4bbb080bd4994ec5a8061e1828aae9a4be812f6f356f1b6fd082bc2d54df7c37163b943a421a8f7f7079
languageName: node
linkType: hard
"fast-copy@npm:^2.1.0": "fast-copy@npm:^2.1.0":
version: 2.1.3 version: 2.1.3
resolution: "fast-copy@npm:2.1.3" resolution: "fast-copy@npm:2.1.3"
@@ -22208,6 +22238,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"foreground-child@npm:^3.1.0":
version: 3.1.1
resolution: "foreground-child@npm:3.1.1"
dependencies:
cross-spawn: ^7.0.0
signal-exit: ^4.0.1
checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0
languageName: node
linkType: hard
"forever-agent@npm:~0.6.1": "forever-agent@npm:~0.6.1":
version: 0.6.1 version: 0.6.1
resolution: "forever-agent@npm:0.6.1" resolution: "forever-agent@npm:0.6.1"
@@ -23419,6 +23459,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"glob@npm:^10.2.5":
version: 10.3.1
resolution: "glob@npm:10.3.1"
dependencies:
foreground-child: ^3.1.0
jackspeak: ^2.0.3
minimatch: ^9.0.1
minipass: ^5.0.0 || ^6.0.2
path-scurry: ^1.10.0
bin:
glob: dist/cjs/src/bin.js
checksum: b39d24c093ce2ffa992dc5b412dbc871af0ccd38a6b2356f67dc906857f0c4c811039a4a4665d19443e1bb484ce2d97855cc7fcfb9a7d0b7e0dadfef4dad5b82
languageName: node
linkType: hard
"glob@npm:^6.0.1": "glob@npm:^6.0.1":
version: 6.0.4 version: 6.0.4
resolution: "glob@npm:6.0.4" resolution: "glob@npm:6.0.4"
@@ -26060,6 +26115,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jackspeak@npm:^2.0.3":
version: 2.2.1
resolution: "jackspeak@npm:2.2.1"
dependencies:
"@isaacs/cliui": ^8.0.2
"@pkgjs/parseargs": ^0.11.0
dependenciesMeta:
"@pkgjs/parseargs":
optional: true
checksum: 510860a5d1eaf12cba509a09a8f7d1696090bfa7c8ae75c6d9c836890d2897409f3b3dd91039cf0020627d6eba8c024f571ae4d78bd956162b07794ddfb9dd62
languageName: node
linkType: hard
"jest-changed-files@npm:^25.5.0": "jest-changed-files@npm:^25.5.0":
version: 25.5.0 version: 25.5.0
resolution: "jest-changed-files@npm:25.5.0" resolution: "jest-changed-files@npm:25.5.0"
@@ -29919,6 +29987,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lru-cache@npm:^9.1.1 || ^10.0.0":
version: 10.0.0
resolution: "lru-cache@npm:10.0.0"
checksum: 347b7b391091e9f91182b6f683ce04329932a542376a2d7d300637213b99f06c222a3bb0f0db59adf246dac6cef1bb509cab352451a96621d07c41b10a20495f
languageName: node
linkType: hard
"lru-queue@npm:^0.1.0": "lru-queue@npm:^0.1.0":
version: 0.1.0 version: 0.1.0
resolution: "lru-queue@npm:0.1.0" resolution: "lru-queue@npm:0.1.0"
@@ -31519,6 +31594,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minimatch@npm:^9.0.1":
version: 9.0.2
resolution: "minimatch@npm:9.0.2"
dependencies:
brace-expansion: ^2.0.1
checksum: 39157d5fd831a7981f7c0c5b22a0e0c2ae8a987ec4a4aeaacc21d3e85da24ce812808cbf7c07cde0d63ad1cf307f73be581131a7a84eeda65f00be1f51972471
languageName: node
linkType: hard
"minimist-options@npm:^4.0.2": "minimist-options@npm:^4.0.2":
version: 4.1.0 version: 4.1.0
resolution: "minimist-options@npm:4.1.0" resolution: "minimist-options@npm:4.1.0"
@@ -31614,6 +31698,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minipass@npm:^5.0.0 || ^6.0.2":
version: 6.0.2
resolution: "minipass@npm:6.0.2"
checksum: 3878076578f44ef4078ceed10af2cfebbec1b6217bf9f7a3d8b940da8153769db29bf88498b2de0d1e0c12dfb7b634c5729b7ca03457f46435e801578add210a
languageName: node
linkType: hard
"minizlib@npm:^1.3.3": "minizlib@npm:^1.3.3":
version: 1.3.3 version: 1.3.3
resolution: "minizlib@npm:1.3.3" resolution: "minizlib@npm:1.3.3"
@@ -33913,6 +34004,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"path-scurry@npm:^1.10.0":
version: 1.10.0
resolution: "path-scurry@npm:1.10.0"
dependencies:
lru-cache: ^9.1.1 || ^10.0.0
minipass: ^5.0.0 || ^6.0.2
checksum: dcc4109928c9a0991f0e1719c73b0a184eb7f313fe3eb2242f25274b1e761f53041989fb6b069541b88f58ee6dfbbecf94922225e0c5a3fba8112c9c60abb391
languageName: node
linkType: hard
"path-to-regexp@npm:0.1.7": "path-to-regexp@npm:0.1.7":
version: 0.1.7 version: 0.1.7
resolution: "path-to-regexp@npm:0.1.7" resolution: "path-to-regexp@npm:0.1.7"
@@ -37479,6 +37580,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rimraf@npm:^5.0.1":
version: 5.0.1
resolution: "rimraf@npm:5.0.1"
dependencies:
glob: ^10.2.5
bin:
rimraf: dist/cjs/src/bin.js
checksum: 9e6062c0aea96f384dd937e6bb06b624c881de2eee79a83d3068193183d44eb9b1f3f68a27a54b9ca8cce56bf34c2951ff4239b093b970e0501a091907031f52
languageName: node
linkType: hard
"rimraf@npm:~2.4.0": "rimraf@npm:~2.4.0":
version: 2.4.5 version: 2.4.5
resolution: "rimraf@npm:2.4.5" resolution: "rimraf@npm:2.4.5"
@@ -38414,6 +38526,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"signal-exit@npm:^4.0.1":
version: 4.0.2
resolution: "signal-exit@npm:4.0.2"
checksum: 3c36ae214f4774b4a7cbbd2d090b2864f8da4dc3f9140ba5b76f38bea7605c7aa8042adf86e48ee8a0955108421873f9b0f20281c61b8a65da4d9c1c1de4929f
languageName: node
linkType: hard
"signedsource@npm:^1.0.0": "signedsource@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "signedsource@npm:1.0.0" resolution: "signedsource@npm:1.0.0"
@@ -39280,7 +39399,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3":
version: 4.2.3 version: 4.2.3
resolution: "string-width@npm:4.2.3" resolution: "string-width@npm:4.2.3"
dependencies: dependencies:
@@ -39302,7 +39421,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"string-width@npm:^5.0.0": "string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2":
version: 5.1.2 version: 5.1.2
resolution: "string-width@npm:5.1.2" resolution: "string-width@npm:5.1.2"
dependencies: dependencies:
@@ -39447,6 +39566,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
version: 6.0.1
resolution: "strip-ansi@npm:6.0.1"
dependencies:
ansi-regex: ^5.0.1
checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952
languageName: node
linkType: hard
"strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": "strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1":
version: 3.0.1 version: 3.0.1
resolution: "strip-ansi@npm:3.0.1" resolution: "strip-ansi@npm:3.0.1"
@@ -39465,15 +39593,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
version: 6.0.1
resolution: "strip-ansi@npm:6.0.1"
dependencies:
ansi-regex: ^5.0.1
checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952
languageName: node
linkType: hard
"strip-ansi@npm:^7.0.1": "strip-ansi@npm:^7.0.1":
version: 7.0.1 version: 7.0.1
resolution: "strip-ansi@npm:7.0.1" resolution: "strip-ansi@npm:7.0.1"
@@ -43203,6 +43322,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
version: 7.0.0
resolution: "wrap-ansi@npm:7.0.0"
dependencies:
ansi-styles: ^4.0.0
string-width: ^4.1.0
strip-ansi: ^6.0.0
checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da
languageName: node
linkType: hard
"wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": "wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0":
version: 6.2.0 version: 6.2.0
resolution: "wrap-ansi@npm:6.2.0" resolution: "wrap-ansi@npm:6.2.0"
@@ -43214,14 +43344,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"wrap-ansi@npm:^7.0.0": "wrap-ansi@npm:^8.1.0":
version: 7.0.0 version: 8.1.0
resolution: "wrap-ansi@npm:7.0.0" resolution: "wrap-ansi@npm:8.1.0"
dependencies: dependencies:
ansi-styles: ^4.0.0 ansi-styles: ^6.1.0
string-width: ^4.1.0 string-width: ^5.0.1
strip-ansi: ^6.0.0 strip-ansi: ^7.0.1
checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60
languageName: node languageName: node
linkType: hard linkType: hard