feat(product): Move mikro orm utils to the utils package (#4631)

Move utils to the utils package as much as possible

Co-authored-by: Shahed Nasser <27354907+shahednasser@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2023-07-31 13:30:43 +02:00
committed by GitHub
parent 648eb106d6
commit 4073b73130
39 changed files with 523 additions and 375 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/product": patch
"@medusajs/utils": patch
"@medusajs/types": patch
---
feat(product): Move mikro orm utils to the utils package
@@ -1,6 +1,6 @@
import { BaseRepository } from "../../src/repositories/base"
import { DALUtils } from "@medusajs/utils"
class CustomRepository extends BaseRepository {
class CustomRepository extends DALUtils.MikroOrmBaseRepository {
constructor({ manager }) {
// @ts-ignore
super(...arguments)
@@ -1,7 +1,6 @@
import { IProductModuleService } from "@medusajs/types"
import { IProductModuleService, ProductTypes } 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"
@@ -43,15 +42,18 @@ describe("ProductModuleService product categories", () => {
status: ProductTypes.ProductStatus.PUBLISHED,
})
const productCategoriesData = [{
id: "test-1",
name: "category 1",
products: [productOne],
},{
id: "test-2",
name: "category",
products: [productTwo],
}]
const productCategoriesData = [
{
id: "test-1",
name: "category 1",
products: [productOne],
},
{
id: "test-2",
name: "category",
products: [productTwo],
},
]
productCategories = await createProductCategories(
testManager,
@@ -121,10 +123,12 @@ describe("ProductModuleService product categories", () => {
expect.objectContaining({
id: "test-1",
name: "category 1",
products: [expect.objectContaining({
id: "product-1",
title: "product 1",
})],
products: [
expect.objectContaining({
id: "product-1",
title: "product 1",
}),
],
}),
])
})
@@ -191,10 +195,12 @@ describe("ProductModuleService product categories", () => {
expect.objectContaining({
id: "test-1",
name: "category 1",
products: [expect.objectContaining({
id: "product-1",
title: "product 1",
})],
products: [
expect.objectContaining({
id: "product-1",
title: "product 1",
}),
],
}),
])
})
@@ -203,35 +209,34 @@ describe("ProductModuleService product categories", () => {
describe("retrieveCategory", () => {
it("should return the requested category", async () => {
const result = await service.retrieveCategory(productCategoryOne.id, {
select: ["id", "name"]
select: ["id", "name"],
})
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"],
}
)
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",
})],
}),
products: [
expect.objectContaining({
id: "product-1",
title: "product 1",
}),
],
})
)
})
@@ -244,7 +249,9 @@ describe("ProductModuleService product categories", () => {
error = e
}
expect(error.message).toEqual("ProductCategory with id: does-not-exist was not found")
expect(error.message).toEqual(
"ProductCategory with id: does-not-exist was not found"
)
})
})
@@ -255,11 +262,14 @@ describe("ProductModuleService product categories", () => {
parent_category_id: productCategoryOne.id,
})
const [productCategory] = await service.listCategories({
name: "New Category"
}, {
select: ["name", "rank"]
})
const [productCategory] = await service.listCategories(
{
name: "New Category",
},
{
select: ["name", "rank"],
}
)
expect(productCategory).toEqual(
expect.objectContaining({
@@ -273,7 +283,7 @@ describe("ProductModuleService product categories", () => {
await service.createCategory({
name: "New Category",
parent_category_id: productCategoryOne.id,
rank: 0
rank: 0,
})
await service.createCategory({
@@ -281,11 +291,14 @@ describe("ProductModuleService product categories", () => {
parent_category_id: productCategoryOne.id,
})
const [productCategoryNew] = await service.listCategories({
name: "New Category 2"
}, {
select: ["name", "rank"]
})
const [productCategoryNew] = await service.listCategories(
{
name: "New Category 2",
},
{
select: ["name", "rank"],
}
)
expect(productCategoryNew).toEqual(
expect.objectContaining({
@@ -299,11 +312,14 @@ describe("ProductModuleService product categories", () => {
parent_category_id: productCategoryNew.id,
})
const [productCategoryWithParent] = await service.listCategories({
name: "New Category 2.1"
}, {
select: ["name", "rank", "parent_category_id"]
})
const [productCategoryWithParent] = await service.listCategories(
{
name: "New Category 2.1",
},
{
select: ["name", "rank", "parent_category_id"],
}
)
expect(productCategoryWithParent).toEqual(
expect.objectContaining({
@@ -342,12 +358,15 @@ describe("ProductModuleService product categories", () => {
it("should update the name of the category successfully", async () => {
await service.updateCategory(productCategoryZero.id, {
name: "New Category"
name: "New Category",
})
const productCategory = await service.retrieveCategory(productCategoryZero.id, {
select: ["name"]
})
const productCategory = await service.retrieveCategory(
productCategoryZero.id,
{
select: ["name"],
}
)
expect(productCategory.name).toEqual("New Category")
})
@@ -357,13 +376,15 @@ describe("ProductModuleService product categories", () => {
try {
await service.updateCategory("does-not-exist", {
name: "New Category"
name: "New Category",
})
} catch (e) {
error = e
}
expect(error.message).toEqual(`ProductCategory not found ({ id: 'does-not-exist' })`)
expect(error.message).toEqual(
`ProductCategory not found ({ id: 'does-not-exist' })`
)
})
it("should reorder rank successfully in the same parent", async () => {
@@ -371,11 +392,14 @@ describe("ProductModuleService product categories", () => {
rank: 0,
})
const productCategories = await service.listCategories({
parent_category_id: null
}, {
select: ["name", "rank"]
})
const productCategories = await service.listCategories(
{
parent_category_id: null,
},
{
select: ["name", "rank"],
}
)
expect(productCategories).toEqual(
expect.arrayContaining([
@@ -390,7 +414,7 @@ describe("ProductModuleService product categories", () => {
expect.objectContaining({
id: productCategoryOne.id,
rank: "2",
})
}),
])
)
})
@@ -398,14 +422,17 @@ describe("ProductModuleService product categories", () => {
it("should reorder rank successfully when changing parent", async () => {
await service.updateCategory(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id
parent_category_id: productCategoryZero.id,
})
const productCategories = await service.listCategories({
parent_category_id: productCategoryZero.id
}, {
select: ["name", "rank"]
})
const productCategories = await service.listCategories(
{
parent_category_id: productCategoryZero.id,
},
{
select: ["name", "rank"],
}
)
expect(productCategories).toEqual(
expect.arrayContaining([
@@ -424,7 +451,7 @@ describe("ProductModuleService product categories", () => {
expect.objectContaining({
id: productCategoryZeroTwo.id,
rank: "3",
})
}),
])
)
})
@@ -432,14 +459,17 @@ describe("ProductModuleService product categories", () => {
it("should reorder rank successfully when changing parent and in first position", async () => {
await service.updateCategory(productCategoryTwo.id, {
rank: 0,
parent_category_id: productCategoryZero.id
parent_category_id: productCategoryZero.id,
})
const productCategories = await service.listCategories({
parent_category_id: productCategoryZero.id
}, {
select: ["name", "rank"]
})
const productCategories = await service.listCategories(
{
parent_category_id: productCategoryZero.id,
},
{
select: ["name", "rank"],
}
)
expect(productCategories).toEqual(
expect.arrayContaining([
@@ -458,7 +488,7 @@ describe("ProductModuleService product categories", () => {
expect.objectContaining({
id: productCategoryZeroTwo.id,
rank: "3",
})
}),
])
)
})
@@ -492,7 +522,9 @@ describe("ProductModuleService product categories", () => {
error = e
}
expect(error.message).toEqual(`ProductCategory not found ({ id: 'does-not-exist' })`)
expect(error.message).toEqual(
`ProductCategory not found ({ id: 'does-not-exist' })`
)
})
it("should throw an error when it has children", async () => {
@@ -504,17 +536,22 @@ describe("ProductModuleService product categories", () => {
error = e
}
expect(error.message).toEqual(`Deleting ProductCategory (category-0-0) with category children is not allowed`)
expect(error.message).toEqual(
`Deleting ProductCategory (category-0-0) with category children is not allowed`
)
})
it("should reorder siblings rank successfully on deleting", async () => {
await service.deleteCategory(productCategoryOne.id)
const productCategories = await service.listCategories({
parent_category_id: null
}, {
select: ["id", "rank"]
})
const productCategories = await service.listCategories(
{
parent_category_id: null,
},
{
select: ["id", "rank"],
}
)
expect(productCategories).toEqual(
expect.arrayContaining([
@@ -525,10 +562,9 @@ describe("ProductModuleService product categories", () => {
expect.objectContaining({
id: productCategoryTwo.id,
rank: "1",
})
}),
])
)
})
})
})
+2 -3
View File
@@ -6,12 +6,11 @@ import {
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
} from "@medusajs/modules-sdk"
import { MedusaError, ModulesSdkUtils } from "@medusajs/utils"
import { DALUtils, MedusaError, ModulesSdkUtils } from "@medusajs/utils"
import { EntitySchema } from "@mikro-orm/core"
import * as ProductModels from "@models"
import { createConnection } from "../utils"
import { ConfigModule, ModulesSdkTypes } from "@medusajs/types"
export default async (
@@ -61,7 +60,7 @@ async function loadDefault({ database, container }) {
}
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(database, entities)
const orm = await DALUtils.mikroOrmCreateConnection(database, entities)
container.register({
manager: asValue(orm.em.fork()),
@@ -1,5 +1,4 @@
import * as DefaultRepositories from "@repositories"
import {
BaseRepository,
ProductCategoryRepository,
@@ -2,6 +2,7 @@ import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
OneToMany,
OptionalProps,
@@ -10,14 +11,13 @@ import {
Unique,
} from "@mikro-orm/core"
import { generateEntityId, kebabCase } from "@medusajs/utils"
import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils"
import Product from "./product"
import { SoftDeletable } from "../utils"
type OptionalRelations = "products"
@Entity({ tableName: "product_collection" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductCollection {
[OptionalProps]?: OptionalRelations
+3 -3
View File
@@ -2,6 +2,7 @@ import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
OptionalProps,
@@ -9,14 +10,13 @@ import {
Property,
} from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import Product from "./product"
import { SoftDeletable } from "../utils"
type OptionalRelations = "products"
@Entity({ tableName: "image" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductImage {
[OptionalProps]?: OptionalRelations
@@ -1,6 +1,7 @@
import {
BeforeCreate,
Entity,
Filter,
Index,
ManyToOne,
OptionalProps,
@@ -8,9 +9,7 @@ import {
Property,
} from "@mikro-orm/core"
import { ProductOption, ProductVariant } from "./index"
import { SoftDeletable } from "../utils"
import { generateEntityId } from "@medusajs/utils"
import { DALUtils, generateEntityId } from "@medusajs/utils"
type OptionalFields =
| "created_at"
@@ -22,7 +21,7 @@ type OptionalFields =
type OptionalRelations = "product" | "option" | "variant"
@Entity({ tableName: "product_option_value" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductOptionValue {
[OptionalProps]?: OptionalFields | OptionalRelations
@@ -1,9 +1,10 @@
import { generateEntityId } from "@medusajs/utils"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToOne,
OneToMany,
@@ -13,13 +14,12 @@ import {
} from "@mikro-orm/core"
import { Product } from "./index"
import ProductOptionValue from "./product-option-value"
import { SoftDeletable } from "../utils"
type OptionalRelations = "values" | "product"
type OptionalFields = "product_id"
@Entity({ tableName: "product_option" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductOption {
[OptionalProps]?: OptionalRelations | OptionalFields
+3 -3
View File
@@ -2,6 +2,7 @@ import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
OptionalProps,
@@ -9,14 +10,13 @@ import {
Property,
} from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import Product from "./product"
import { SoftDeletable } from "../utils"
type OptionalRelations = "products"
@Entity({ tableName: "product_tag" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductTag {
[OptionalProps]?: OptionalRelations
+3 -3
View File
@@ -1,16 +1,16 @@
import {
BeforeCreate,
Entity,
Filter,
Index,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils"
import { SoftDeletable } from "../utils"
import { DALUtils, generateEntityId } from "@medusajs/utils"
@Entity({ tableName: "product_type" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductType {
@PrimaryKey({ columnType: "text" })
id!: string
@@ -1,9 +1,10 @@
import { generateEntityId } from "@medusajs/utils"
import { DALUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToOne,
OneToMany,
@@ -14,7 +15,6 @@ import {
} from "@mikro-orm/core"
import { Product } from "@models"
import ProductOptionValue from "./product-option-value"
import { SoftDeletable } from "../utils"
type OptionalFields =
| "created_at"
@@ -25,7 +25,7 @@ type OptionalFields =
| "product_id"
@Entity({ tableName: "product_variant" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductVariant {
[OptionalProps]?: OptionalFields
+3 -3
View File
@@ -3,6 +3,7 @@ import {
Collection,
Entity,
Enum,
Filter,
Index,
ManyToMany,
ManyToOne,
@@ -14,7 +15,7 @@ import {
} from "@mikro-orm/core"
import { ProductTypes } from "@medusajs/types"
import { generateEntityId, kebabCase } from "@medusajs/utils"
import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils"
import ProductCategory from "./product-category"
import ProductCollection from "./product-collection"
import ProductOption from "./product-option"
@@ -22,7 +23,6 @@ import ProductTag from "./product-tag"
import ProductType from "./product-type"
import ProductVariant from "./product-variant"
import ProductImage from "./product-image"
import { SoftDeletable } from "../utils"
type OptionalRelations = "collection" | "type"
type OptionalFields =
@@ -34,7 +34,7 @@ type OptionalFields =
| "updated_at"
@Entity({ tableName: "product" })
@SoftDeletable()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class Product {
[OptionalProps]?: OptionalRelations | OptionalFields
+1 -1
View File
@@ -1,4 +1,4 @@
export { BaseRepository } from "./base"
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export { ProductRepository } from "./product"
export { ProductTagRepository } from "./product-tag"
export { ProductVariantRepository } from "./product-variant"
@@ -3,12 +3,17 @@ import {
FindOptions as MikroOptions,
LoadStrategy,
} from "@mikro-orm/core"
import { Product, ProductCategory } from "@models"
import { ProductCategory } from "@models"
import { Context, DAL, ProductCategoryTransformOptions } from "@medusajs/types"
import groupBy from "lodash/groupBy"
import { BaseTreeRepository } from "./base"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { InjectTransactionManager, MedusaContext, isDefined, MedusaError } from "@medusajs/utils"
import {
DALUtils,
InjectTransactionManager,
isDefined,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { ProductCategoryServiceTypes } from "../types"
@@ -25,7 +30,7 @@ export type ReorderConditions = {
}
export const tempReorderRank = 99999
export class ProductCategoryRepository extends BaseTreeRepository {
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -209,12 +214,9 @@ export class ProductCategoryRepository extends BaseTreeRepository {
): Promise<ProductCategory> {
const categoryData = { ...data }
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
const siblings = await manager.find(
ProductCategory,
{
parent_category_id: categoryData?.parent_category_id || null
},
)
const siblings = await manager.find(ProductCategory, {
parent_category_id: categoryData?.parent_category_id || null,
})
if (!isDefined(categoryData.rank)) {
categoryData.rank = siblings.length
@@ -343,25 +345,19 @@ export class ProductCategoryRepository extends BaseTreeRepository {
// The current sibling count will replace targetRank if
// targetRank is greater than the count of siblings.
const siblingCount = await manager.count(
ProductCategory,
{
parent_category_id: targetParentId || null,
id: { $ne: targetCategoryId },
}
)
const siblingCount = await manager.count(ProductCategory, {
parent_category_id: targetParentId || null,
id: { $ne: targetCategoryId },
})
// The category record that will be placed at the requested rank
// We've temporarily placed it at a temporary rank that is
// beyond a reasonable value (tempReorderRank)
const targetCategory = await manager.findOne(
ProductCategory,
{
id: targetCategoryId,
parent_category_id: targetParentId || null,
rank: tempReorderRank,
}
)
const targetCategory = await manager.findOne(ProductCategory, {
id: targetCategoryId,
parent_category_id: targetParentId || null,
rank: tempReorderRank,
})
// If the targetRank is not present, or if targetRank is beyond the
// rank of the last category, we set the rank as the last rank
@@ -7,14 +7,13 @@ import {
import { Context, DAL, ProductTypes } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
DALUtils,
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { BaseRepository } from "./base"
export class ProductCollectionRepository extends BaseRepository {
export class ProductCollectionRepository extends DALUtils.MikroOrmBaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -5,11 +5,14 @@ import {
} 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"
import {
DALUtils,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/utils"
export class ProductImageRepository extends AbstractBaseRepository<Image> {
export class ProductImageRepository extends DALUtils.MikroOrmAbstractBaseRepository<Image> {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -5,15 +5,15 @@ import {
} 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 {
DALUtils,
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
export class ProductOptionRepository extends AbstractBaseRepository<ProductOption> {
export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseRepository<ProductOption> {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -88,13 +88,15 @@ export class ProductOptionRepository extends AbstractBaseRepository<ProductOptio
data.forEach((d) => d.product_id && productIds.push(d.product_id))
const existingProducts = await manager.find(
Product,
{ id: { $in: productIds } },
)
const existingProducts = await manager.find(Product, {
id: { $in: productIds },
})
const existingProductsMap = new Map(
existingProducts.map<[string, Product]>((product) => [product.id, product])
existingProducts.map<[string, Product]>((product) => [
product.id,
product,
])
)
const productOptions = data.map((optionData) => {
@@ -136,7 +138,10 @@ export class ProductOptionRepository extends AbstractBaseRepository<ProductOptio
)
const existingOptionsMap = new Map(
existingOptions.map<[string, ProductOption]>((option) => [option.id, option])
existingOptions.map<[string, ProductOption]>((option) => [
option.id,
option,
])
)
const productOptions = data.map((optionData) => {
@@ -12,15 +12,15 @@ import {
UpdateProductTagDTO,
UpsertProductTagDTO,
} from "@medusajs/types"
import { BaseRepository } from "./base"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
DALUtils,
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
export class ProductTagRepository extends BaseRepository {
export class ProductTagRepository extends DALUtils.MikroOrmBaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -13,14 +13,13 @@ import {
} from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
DALUtils,
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { BaseRepository } from "./base"
export class ProductTypeRepository extends BaseRepository {
export class ProductTypeRepository extends DALUtils.MikroOrmBaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -8,17 +8,15 @@ import { ProductVariant } from "@models"
import { Context, DAL, WithRequiredProperty } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
MedusaError,
isDefined,
DALUtils,
InjectTransactionManager,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { ProductVariantServiceTypes } from "../types/services"
import { AbstractBaseRepository } from "./base"
import { doNotForceTransaction } from "../utils"
export class ProductVariantRepository extends AbstractBaseRepository<ProductVariant> {
export class ProductVariantRepository extends DALUtils.MikroOrmAbstractBaseRepository<ProductVariant> {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -96,14 +94,17 @@ export class ProductVariantRepository extends AbstractBaseRepository<ProductVari
}
async update(
data: WithRequiredProperty<ProductVariantServiceTypes.UpdateProductVariantDTO, "id">[],
data: WithRequiredProperty<
ProductVariantServiceTypes.UpdateProductVariantDTO,
"id"
>[],
context: Context = {}
): Promise<ProductVariant[]> {
const manager = (context.transactionManager ??
this.manager_) as SqlEntityManager
const productVariantsToUpdate = await manager.find(ProductVariant, {
id: data.map((updateData) => updateData.id)
id: data.map((updateData) => updateData.id),
})
const productVariantsToUpdateMap = new Map<string, ProductVariant>(
+56 -37
View File
@@ -2,15 +2,14 @@ import {
Product,
ProductCategory,
ProductCollection,
ProductType,
ProductTag,
ProductType,
} from "@models"
import {
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
LoadStrategy,
wrap
} from "@mikro-orm/core"
import {
@@ -20,12 +19,17 @@ import {
WithRequiredProperty,
} from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { MedusaError, isDefined, InjectTransactionManager, MedusaContext } from "@medusajs/utils"
import {
DALUtils,
InjectTransactionManager,
isDefined,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { AbstractBaseRepository } from "./base"
import { ProductServiceTypes } from "../types/services"
export class ProductRepository extends AbstractBaseRepository<Product> {
export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<Product> {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
@@ -157,12 +161,10 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
data.forEach((productData) => {
categoryIds = categoryIds.concat(
productData?.categories?.map(c => c.id) || []
productData?.categories?.map((c) => c.id) || []
)
tagIds = tagIds.concat(
productData?.tags?.map(c => c.id) || []
)
tagIds = tagIds.concat(productData?.tags?.map((c) => c.id) || [])
if (productData.collection_id) {
collectionIds.push(productData.collection_id)
@@ -173,27 +175,39 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
}
})
const productsToUpdate = await manager.find(Product, {
id: data.map((updateData) => updateData.id)
}, {
populate: ["tags", "categories"]
})
const productsToUpdate = await manager.find(
Product,
{
id: data.map((updateData) => updateData.id),
},
{
populate: ["tags", "categories"],
}
)
const collectionsToAssign = collectionIds.length ? await manager.find(ProductCollection, {
id: collectionIds
}) : []
const collectionsToAssign = collectionIds.length
? await manager.find(ProductCollection, {
id: collectionIds,
})
: []
const typesToAssign = typeIds.length ? await manager.find(ProductType, {
id: typeIds
}) : []
const typesToAssign = typeIds.length
? await manager.find(ProductType, {
id: typeIds,
})
: []
const categoriesToAssign = categoryIds.length ? await manager.find(ProductCategory, {
id: categoryIds
}) : []
const categoriesToAssign = categoryIds.length
? await manager.find(ProductCategory, {
id: categoryIds,
})
: []
const tagsToAssign = tagIds.length ? await manager.find(ProductTag, {
id: tagIds
}) : []
const tagsToAssign = tagIds.length
? await manager.find(ProductTag, {
id: tagIds,
})
: []
const categoriesToAssignMap = new Map<string, ProductCategory>(
categoriesToAssign.map((category) => [category.id, category])
@@ -227,8 +241,8 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
}
const {
categories: categoriesData,
tags: tagsData,
categories: categoriesData = [],
tags: tagsData = [],
collection_id: collectionId,
type_id: typeId,
} = updateData
@@ -249,10 +263,15 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
}
}
const categoryIdsToAssignSet = new Set(categoriesData.map(cd => cd.id))
const categoriesToDelete = product.categories.getItems().filter(
(existingCategory) => !categoryIdsToAssignSet.has(existingCategory.id)
const categoryIdsToAssignSet = new Set(
categoriesData.map((cd) => cd.id)
)
const categoriesToDelete = product.categories
.getItems()
.filter(
(existingCategory) =>
!categoryIdsToAssignSet.has(existingCategory.id)
)
await product.categories.remove(categoriesToDelete)
}
@@ -272,22 +291,22 @@ export class ProductRepository extends AbstractBaseRepository<Product> {
}
}
const tagIdsToAssignSet = new Set(tagsData.map(cd => cd.id))
const tagsToDelete = product.tags.getItems().filter(
(existingTag) => !tagIdsToAssignSet.has(existingTag.id)
)
const tagIdsToAssignSet = new Set(tagsData.map((cd) => cd.id))
const tagsToDelete = product.tags
.getItems()
.filter((existingTag) => !tagIdsToAssignSet.has(existingTag.id))
await product.tags.remove(tagsToDelete)
}
if (isDefined(collectionId)) {
const collection = collectionsToAssignMap.get(collectionId)
const collection = collectionsToAssignMap.get(collectionId!)
product.collection = collection || null
}
if (isDefined(typeId)) {
const type = typesToAssignMap.get(typeId)
const type = typesToAssignMap.get(typeId!)
if (type) {
product.type = type
@@ -3,8 +3,7 @@ import * as ProductModels from "@models"
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { EntitySchema } from "@mikro-orm/core"
import { ModulesSdkUtils } from "@medusajs/utils"
import { createConnection } from "../utils"
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
/**
* This script is only valid for mikro orm managers. If a user provide a custom manager
@@ -28,7 +27,7 @@ export async function revertMigration({
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(dbData, entities)
const orm = await DALUtils.mikroOrmCreateConnection(dbData, entities)
try {
const migrator = orm.getMigrator()
+2 -3
View File
@@ -1,8 +1,7 @@
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { createConnection } from "../utils"
import * as ProductModels from "@models"
import { EntitySchema } from "@mikro-orm/core"
import { ModulesSdkUtils } from "@medusajs/utils"
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
/**
* This script is only valid for mikro orm managers. If a user provide a custom manager
@@ -26,7 +25,7 @@ export async function runMigrations({
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(dbData, entities)
const orm = await DALUtils.mikroOrmCreateConnection(dbData, entities)
try {
const migrator = orm.getMigrator()
+2 -3
View File
@@ -1,4 +1,3 @@
import { createConnection } from "../utils"
import * as ProductModels from "@models"
import { Product, ProductCategory, ProductVariant } from "@models"
import { EntitySchema } from "@mikro-orm/core"
@@ -6,7 +5,7 @@ import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { EOL } from "os"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { resolve } from "path"
import { ModulesSdkUtils } from "@medusajs/utils"
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
export async function run({
options,
@@ -38,7 +37,7 @@ export async function run({
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
const orm = await createConnection(dbData, entities)
const orm = await DALUtils.mikroOrmCreateConnection(dbData, entities)
const manager = orm.em.fork()
try {
-3
View File
@@ -1,8 +1,5 @@
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
export * from "./create-connection"
export * from "./soft-deletable"
export function shouldForceTransaction(target: any): boolean {
return target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
}
@@ -1,23 +0,0 @@
// TODO: Should we create a mikro orm specific package for this and the base repository?
import { Filter } from "@mikro-orm/core"
import { SoftDeletableFilterKey } from "@medusajs/utils"
interface FilterArguments {
withDeleted?: boolean
}
export const SoftDeletable = (): ClassDecorator => {
return Filter({
name: SoftDeletableFilterKey,
cond: ({ withDeleted }: FilterArguments = {}) => {
if (withDeleted) {
return {}
}
return {
deleted_at: null,
}
},
default: true,
})
}
+3 -6
View File
@@ -35,13 +35,9 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<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[]>
// TODO: remove optionality when all the other repositories have an update
update?(data: unknown[], context?: Context): Promise<T[]>
update(data: unknown[], context?: Context): Promise<T[]>
delete(ids: string[], context?: Context): Promise<void>
@@ -50,7 +46,8 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
restore(ids: string[], context?: Context): Promise<T[]>
}
export interface TreeRepositoryService<T = any> extends BaseRepositoryService<T> {
export interface TreeRepositoryService<T = any>
extends BaseRepositoryService<T> {
find(
options?: FindOptions<T>,
transformOptions?: RepositoryTransformOptions,
+1
View File
@@ -2,3 +2,4 @@ export * as DecoratorUtils from "./decorators"
export * as EventBusUtils from "./event-bus"
export * as SearchUtils from "./search"
export * as ModulesSdkUtils from "./modules-sdk"
export * as DALUtils from "./dal"
-1
View File
@@ -1 +0,0 @@
export const SoftDeletableFilterKey = "softDeletable"
-1
View File
@@ -22,4 +22,3 @@ export * from "./stringify-circular"
export * from "./to-kebab-case"
export * from "./to-pascal-case"
export * from "./wrap-handler"
export * from "./dal"
+5
View File
@@ -0,0 +1,5 @@
export * from "./mikro-orm/mikro-orm-repository"
export * from "./repository"
export * from "./utils"
export * from "./mikro-orm/mikro-orm-create-connection"
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
@@ -1,12 +1,13 @@
import { MikroORM, PostgreSqlDriver } from "@mikro-orm/postgresql"
import { ModuleServiceInitializeOptions } from "@medusajs/types"
export async function createConnection(
export async function mikroOrmCreateConnection(
database: ModuleServiceInitializeOptions["database"],
entities: any[]
) {
const { MikroORM } = await import("@mikro-orm/postgresql")
const schema = database.schema || "public"
const orm = await MikroORM.init<PostgreSqlDriver>({
const orm = await MikroORM.init({
discovery: { disableDynamicFileAccess: true },
entities,
debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false,
@@ -1,100 +1,14 @@
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
import { SoftDeletableFilterKey } from "@medusajs/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { MedusaContext } from "../../decorators"
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
import {
buildQuery,
InjectTransactionManager,
MedusaContext,
} from "@medusajs/utils"
import { serialize } from "@mikro-orm/core"
mikroOrmSerializer,
mikroOrmUpdateDeletedAtRecursively,
transactionWrapper,
} from "../utils"
// TODO: move to utils package
async function transactionWrapper<TManager = unknown>(
this: any,
task: (transactionManager: unknown) => Promise<any>,
{
transaction,
isolationLevel,
enableNestedTransactions = false,
}: {
isolationLevel?: string
transaction?: TManager
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 options = {}
if (transaction) {
Object.assign(options, { ctx: transaction })
}
if (isolationLevel) {
Object.assign(options, { isolationLevel })
}
return await (this.manager_ as SqlEntityManager).transactional(task, options)
}
// TODO: move to utils package
const mikroOrmUpdateDeletedAtRecursively = async <T extends object = any>(
manager: any,
entities: T[],
value: Date | null
) => {
for (const entity of entities) {
if (!("deleted_at" in entity)) continue
;(entity as any).deleted_at = value
const relations = manager
.getDriver()
.getMetadata()
.get(entity.constructor.name).relations
const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any)
)
for (const relation of relationsToCascade) {
let collectionRelation = entity[relation.name]
if (!collectionRelation.isInitialized()) {
await collectionRelation.init()
}
const relationEntities = await collectionRelation.getItems({
filters: {
[SoftDeletableFilterKey]: {
withDeleted: true,
},
},
})
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
}
await manager.persist(entity)
}
}
const serializer = <TOutput extends object>(
data: any,
options?: any
): Promise<TOutput> => {
options ??= {}
const result = serialize(data, options)
return result as unknown as Promise<TOutput>
}
// TODO: move to utils package
class AbstractBase<T = any> {
protected readonly manager_: SqlEntityManager
class MikroOrmBase<T = any> {
protected readonly manager_: any
protected constructor({ manager }) {
this.manager_ = manager
@@ -115,29 +29,26 @@ class AbstractBase<T = any> {
async transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>,
{
transaction,
isolationLevel,
enableNestedTransactions = false,
}: {
options: {
isolationLevel?: string
enableNestedTransactions?: boolean
transaction?: TManager
} = {}
): Promise<any> {
return await transactionWrapper.apply(this, arguments)
// @ts-ignore
return await transactionWrapper.bind(this)(task, options)
}
async serialize<TOutput extends object | object[]>(
data: any,
options?: any
): Promise<TOutput> {
return await serializer<TOutput>(data, options)
return await mikroOrmSerializer<TOutput>(data, options)
}
}
export abstract class AbstractBaseRepository<T = any>
extends AbstractBase
export abstract class MikroOrmAbstractBaseRepository<T = any>
extends MikroOrmBase
implements DAL.RepositoryService<T>
{
abstract find(options?: DAL.FindOptions<T>, context?: Context)
@@ -149,6 +60,10 @@ export abstract class AbstractBaseRepository<T = any>
abstract create(data: unknown[], context?: Context): Promise<T[]>
update(data: unknown[], context?: Context): Promise<T[]> {
throw new Error("Method not implemented.")
}
abstract delete(ids: string[], context?: Context): Promise<void>
@InjectTransactionManager()
@@ -160,11 +75,7 @@ export abstract class AbstractBaseRepository<T = any>
const entities = await this.find({ where: { id: { $in: ids } } as any })
const date = new Date()
await mikroOrmUpdateDeletedAtRecursively(
manager as SqlEntityManager,
entities,
date
)
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
return entities
}
@@ -173,7 +84,7 @@ export abstract class AbstractBaseRepository<T = any>
async restore(
ids: string[],
@MedusaContext()
{ transactionManager: manager }: Context<SqlEntityManager> = {}
{ transactionManager: manager }: Context = {}
): Promise<T[]> {
const query = buildQuery(
{ id: { $in: ids } },
@@ -184,19 +95,14 @@ export abstract class AbstractBaseRepository<T = any>
const entities = await this.find(query)
await mikroOrmUpdateDeletedAtRecursively(
manager as SqlEntityManager,
entities,
null
)
await mikroOrmUpdateDeletedAtRecursively(manager, entities, null)
return entities
}
}
// TODO: move to utils package
export abstract class AbstractTreeRepositoryBase<T = any>
extends AbstractBase<T>
export abstract class MikroOrmAbstractTreeRepositoryBase<T = any>
extends MikroOrmBase<T>
implements DAL.TreeRepositoryService<T>
{
protected constructor({ manager }) {
@@ -221,13 +127,15 @@ export abstract class AbstractTreeRepositoryBase<T = any>
abstract delete(id: string, context?: Context): Promise<void>
}
// TODO: move to utils package
/**
* Only used internally in order to be able to wrap in transaction from a
* non identified repository
* Priviliged extends of the abstract classes unless most of the methods can't be implemented
* in your repository. This base repository is also used to provide a base repository
* injection if needed to be able to use the common methods without being related to an entity.
* In this case, none of the method will be implemented except the manager and transaction
* related ones.
*/
export class BaseRepository extends AbstractBaseRepository {
export class MikroOrmBaseRepository extends MikroOrmAbstractBaseRepository {
constructor({ manager }) {
// @ts-ignore
super(...arguments)
@@ -237,6 +145,10 @@ export class BaseRepository extends AbstractBaseRepository {
throw new Error("Method not implemented.")
}
update(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.")
}
@@ -253,8 +165,7 @@ export class BaseRepository extends AbstractBaseRepository {
}
}
export class BaseTreeRepository extends AbstractTreeRepositoryBase {
export class MikroOrmBaseTreeRepository extends MikroOrmAbstractTreeRepositoryBase {
constructor({ manager }) {
// @ts-ignore
super(...arguments)
@@ -0,0 +1,19 @@
export const SoftDeletableFilterKey = "softDeletable"
interface FilterArguments {
withDeleted?: boolean
}
export const mikroOrmSoftDeletableFilterOptions = {
name: SoftDeletableFilterKey,
cond: ({ withDeleted }: FilterArguments = {}) => {
if (withDeleted) {
return {}
}
return {
deleted_at: null,
}
},
default: true,
args: false,
}
+95
View File
@@ -0,0 +1,95 @@
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
import { MedusaContext } from "../decorators"
class AbstractBase<T = any> {
protected readonly manager_: any
protected constructor({ manager }) {
this.manager_ = manager
}
getActiveManager<TManager = unknown>(
@MedusaContext()
{ transactionManager, manager }: Context = {}
): TManager {
return (transactionManager ?? manager ?? this.manager_) as TManager
}
async transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>,
{
transaction,
isolationLevel,
enableNestedTransactions = false,
}: {
isolationLevel?: string
enableNestedTransactions?: boolean
transaction?: TManager
} = {}
): Promise<any> {
// @ts-ignore
return await transactionWrapper.apply(this, arguments)
}
}
export abstract class AbstractBaseRepository<T = any>
extends AbstractBase
implements DAL.RepositoryService<T>
{
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 update(data: unknown[], context?: Context): Promise<T[]>
abstract delete(ids: string[], context?: Context): Promise<void>
abstract softDelete(ids: string[], context?: Context): Promise<T[]>
abstract restore(ids: string[], context?: Context): Promise<T[]>
abstract getFreshManager<TManager = unknown>(): TManager
abstract serialize<TOutput extends object | object[]>(
data: any,
options?: any
): Promise<TOutput>
}
export abstract class AbstractTreeRepositoryBase<T = any>
extends AbstractBase<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]>
abstract create(data: unknown, context?: Context): Promise<T>
abstract delete(id: string, context?: Context): Promise<void>
abstract getFreshManager<TManager = unknown>(): TManager
abstract serialize<TOutput extends object | object[]>(
data: any,
options?: any
): Promise<TOutput>
}
+86
View File
@@ -0,0 +1,86 @@
import { SoftDeletableFilterKey } from "../dal"
export async function transactionWrapper<TManager = unknown>(
this: any,
task: (transactionManager: unknown) => Promise<any>,
{
transaction,
isolationLevel,
enableNestedTransactions = false,
}: {
isolationLevel?: string
transaction?: TManager
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 options = {}
if (transaction) {
Object.assign(options, { ctx: transaction })
}
if (isolationLevel) {
Object.assign(options, { isolationLevel })
}
const transactionMethod =
this.manager_.transaction ?? this.manager_.transactional
return await transactionMethod.bind(this.manager_)(task, options)
}
export const mikroOrmUpdateDeletedAtRecursively = async <
T extends object = any
>(
manager: any,
entities: T[],
value: Date | null
) => {
for (const entity of entities) {
if (!("deleted_at" in entity)) continue
;(entity as any).deleted_at = value
const relations = manager
.getDriver()
.getMetadata()
.get(entity.constructor.name).relations
const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any)
)
for (const relation of relationsToCascade) {
let collectionRelation = entity[relation.name]
if (!collectionRelation.isInitialized()) {
await collectionRelation.init()
}
const relationEntities = await collectionRelation.getItems({
filters: {
[SoftDeletableFilterKey]: {
withDeleted: true,
},
},
})
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
}
await manager.persist(entity)
}
}
export const mikroOrmSerializer = async <TOutput extends object>(
data: any,
options?: any
): Promise<TOutput> => {
options ??= {}
const { serialize } = await import("@mikro-orm/core")
const result = serialize(data, options)
return result as unknown as Promise<TOutput>
}
+1
View File
@@ -5,3 +5,4 @@ export * from "./decorators"
export * from "./event-bus"
export * from "./search"
export * from "./modules-sdk"
export * from "./dal"
@@ -1,6 +1,7 @@
import { DAL, FindConfig } from "@medusajs/types"
import { deduplicate, isObject, SoftDeletableFilterKey } from "../common"
import { deduplicate, isObject } from "../common"
import { SoftDeletableFilterKey } from "../dal"
export function buildQuery<T = any, TDto = any>(
filters: Record<string, any> = {},