From 4073b73130c874dc7d2240726224a01b7b19b1a1 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 31 Jul 2023 13:30:43 +0200 Subject: [PATCH] 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> --- .changeset/tender-pans-leave.md | 7 + .../integration-tests/__fixtures__/module.ts | 4 +- .../product-categories.spec.ts | 206 ++++++++++-------- packages/product/src/loaders/connection.ts | 5 +- packages/product/src/loaders/container.ts | 1 - .../product/src/models/product-collection.ts | 6 +- packages/product/src/models/product-image.ts | 6 +- .../src/models/product-option-value.ts | 7 +- packages/product/src/models/product-option.ts | 6 +- packages/product/src/models/product-tag.ts | 6 +- packages/product/src/models/product-type.ts | 6 +- .../product/src/models/product-variant.ts | 6 +- packages/product/src/models/product.ts | 6 +- packages/product/src/repositories/index.ts | 2 +- .../src/repositories/product-category.ts | 46 ++-- .../src/repositories/product-collection.ts | 5 +- .../product/src/repositories/product-image.ts | 9 +- .../src/repositories/product-option.ts | 21 +- .../product/src/repositories/product-tag.ts | 4 +- .../product/src/repositories/product-type.ts | 5 +- .../src/repositories/product-variant.ts | 15 +- packages/product/src/repositories/product.ts | 93 ++++---- .../product/src/scripts/migration-down.ts | 5 +- packages/product/src/scripts/migration-up.ts | 5 +- packages/product/src/scripts/seed.ts | 5 +- packages/product/src/utils/index.ts | 3 - packages/product/src/utils/soft-deletable.ts | 23 -- packages/types/src/dal/repository-service.ts | 9 +- packages/utils/src/bundles.ts | 1 + packages/utils/src/common/dal.ts | 1 - packages/utils/src/common/index.ts | 1 - packages/utils/src/dal/index.ts | 5 + .../mikro-orm/mikro-orm-create-connection.ts} | 7 +- .../dal/mikro-orm/mikro-orm-repository.ts} | 157 +++---------- .../mikro-orm-soft-deletable-filter.ts | 19 ++ packages/utils/src/dal/repository.ts | 95 ++++++++ packages/utils/src/dal/utils.ts | 86 ++++++++ packages/utils/src/index.ts | 1 + packages/utils/src/modules-sdk/build-query.ts | 3 +- 39 files changed, 523 insertions(+), 375 deletions(-) create mode 100644 .changeset/tender-pans-leave.md delete mode 100644 packages/product/src/utils/soft-deletable.ts delete mode 100644 packages/utils/src/common/dal.ts create mode 100644 packages/utils/src/dal/index.ts rename packages/{product/src/utils/create-connection.ts => utils/src/dal/mikro-orm/mikro-orm-create-connection.ts} (80%) rename packages/{product/src/repositories/base.ts => utils/src/dal/mikro-orm/mikro-orm-repository.ts} (52%) create mode 100644 packages/utils/src/dal/mikro-orm/mikro-orm-soft-deletable-filter.ts create mode 100644 packages/utils/src/dal/repository.ts create mode 100644 packages/utils/src/dal/utils.ts diff --git a/.changeset/tender-pans-leave.md b/.changeset/tender-pans-leave.md new file mode 100644 index 0000000000..4b74bd38da --- /dev/null +++ b/.changeset/tender-pans-leave.md @@ -0,0 +1,7 @@ +--- +"@medusajs/product": patch +"@medusajs/utils": patch +"@medusajs/types": patch +--- + +feat(product): Move mikro orm utils to the utils package diff --git a/packages/product/integration-tests/__fixtures__/module.ts b/packages/product/integration-tests/__fixtures__/module.ts index 017c3cbaf8..19fa47e724 100644 --- a/packages/product/integration-tests/__fixtures__/module.ts +++ b/packages/product/integration-tests/__fixtures__/module.ts @@ -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) diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts index 8ac9d6f03b..1f5d4a0aec 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/product-categories.spec.ts @@ -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", - }) + }), ]) ) }) }) }) - diff --git a/packages/product/src/loaders/connection.ts b/packages/product/src/loaders/connection.ts index d42d4f3237..223dfcff9d 100644 --- a/packages/product/src/loaders/connection.ts +++ b/packages/product/src/loaders/connection.ts @@ -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()), diff --git a/packages/product/src/loaders/container.ts b/packages/product/src/loaders/container.ts index e3f490a991..23590128d7 100644 --- a/packages/product/src/loaders/container.ts +++ b/packages/product/src/loaders/container.ts @@ -1,5 +1,4 @@ import * as DefaultRepositories from "@repositories" - import { BaseRepository, ProductCategoryRepository, diff --git a/packages/product/src/models/product-collection.ts b/packages/product/src/models/product-collection.ts index 6f364daa2c..68edb7fb7d 100644 --- a/packages/product/src/models/product-collection.ts +++ b/packages/product/src/models/product-collection.ts @@ -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 diff --git a/packages/product/src/models/product-image.ts b/packages/product/src/models/product-image.ts index a142a186e7..adeb233502 100644 --- a/packages/product/src/models/product-image.ts +++ b/packages/product/src/models/product-image.ts @@ -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 diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index 4eea997655..655102fee4 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -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 diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index d05999ae3e..09600ee27d 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -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 diff --git a/packages/product/src/models/product-tag.ts b/packages/product/src/models/product-tag.ts index aceee12e4d..db3bba8b52 100644 --- a/packages/product/src/models/product-tag.ts +++ b/packages/product/src/models/product-tag.ts @@ -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 diff --git a/packages/product/src/models/product-type.ts b/packages/product/src/models/product-type.ts index e2c4616e3a..fd6e460275 100644 --- a/packages/product/src/models/product-type.ts +++ b/packages/product/src/models/product-type.ts @@ -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 diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index 5e754d698d..6ba319f753 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -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 diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 3528f4610a..abad79a0cd 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -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 diff --git a/packages/product/src/repositories/index.ts b/packages/product/src/repositories/index.ts index bc8199a386..77e7f8a7a1 100644 --- a/packages/product/src/repositories/index.ts +++ b/packages/product/src/repositories/index.ts @@ -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" diff --git a/packages/product/src/repositories/product-category.ts b/packages/product/src/repositories/product-category.ts index 309715b430..1295cd48c1 100644 --- a/packages/product/src/repositories/product-category.ts +++ b/packages/product/src/repositories/product-category.ts @@ -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 { const categoryData = { ...data } const manager = this.getActiveManager(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 diff --git a/packages/product/src/repositories/product-collection.ts b/packages/product/src/repositories/product-collection.ts index 1c223926f4..d5825f96aa 100644 --- a/packages/product/src/repositories/product-collection.ts +++ b/packages/product/src/repositories/product-collection.ts @@ -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 }) { diff --git a/packages/product/src/repositories/product-image.ts b/packages/product/src/repositories/product-image.ts index 1c664c610b..8df0c2f154 100644 --- a/packages/product/src/repositories/product-image.ts +++ b/packages/product/src/repositories/product-image.ts @@ -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 { +export class ProductImageRepository extends DALUtils.MikroOrmAbstractBaseRepository { protected readonly manager_: SqlEntityManager constructor({ manager }: { manager: SqlEntityManager }) { diff --git a/packages/product/src/repositories/product-option.ts b/packages/product/src/repositories/product-option.ts index 4751e885b3..937d6334a8 100644 --- a/packages/product/src/repositories/product-option.ts +++ b/packages/product/src/repositories/product-option.ts @@ -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 { +export class ProductOptionRepository extends DALUtils.MikroOrmAbstractBaseRepository { protected readonly manager_: SqlEntityManager constructor({ manager }: { manager: SqlEntityManager }) { @@ -88,13 +88,15 @@ export class ProductOptionRepository extends AbstractBaseRepository 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((option) => [option.id, option]) + existingOptions.map<[string, ProductOption]>((option) => [ + option.id, + option, + ]) ) const productOptions = data.map((optionData) => { diff --git a/packages/product/src/repositories/product-tag.ts b/packages/product/src/repositories/product-tag.ts index 124c20f541..d29f185776 100644 --- a/packages/product/src/repositories/product-tag.ts +++ b/packages/product/src/repositories/product-tag.ts @@ -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 }) { diff --git a/packages/product/src/repositories/product-type.ts b/packages/product/src/repositories/product-type.ts index 8e99eb2f85..132a9e021d 100644 --- a/packages/product/src/repositories/product-type.ts +++ b/packages/product/src/repositories/product-type.ts @@ -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 }) { diff --git a/packages/product/src/repositories/product-variant.ts b/packages/product/src/repositories/product-variant.ts index b6827ed640..96500be02b 100644 --- a/packages/product/src/repositories/product-variant.ts +++ b/packages/product/src/repositories/product-variant.ts @@ -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 { +export class ProductVariantRepository extends DALUtils.MikroOrmAbstractBaseRepository { protected readonly manager_: SqlEntityManager constructor({ manager }: { manager: SqlEntityManager }) { @@ -96,14 +94,17 @@ export class ProductVariantRepository extends AbstractBaseRepository[], + data: WithRequiredProperty< + ProductVariantServiceTypes.UpdateProductVariantDTO, + "id" + >[], context: Context = {} ): Promise { 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( diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts index ee0b9ed732..43899ecfa6 100644 --- a/packages/product/src/repositories/product.ts +++ b/packages/product/src/repositories/product.ts @@ -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 { +export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository { protected readonly manager_: SqlEntityManager constructor({ manager }: { manager: SqlEntityManager }) { @@ -157,12 +161,10 @@ export class ProductRepository extends AbstractBaseRepository { 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 { } }) - 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( categoriesToAssign.map((category) => [category.id, category]) @@ -227,8 +241,8 @@ export class ProductRepository extends AbstractBaseRepository { } 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 { } } - 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 { } } - 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 diff --git a/packages/product/src/scripts/migration-down.ts b/packages/product/src/scripts/migration-down.ts index a67de38c71..3aca80cf60 100644 --- a/packages/product/src/scripts/migration-down.ts +++ b/packages/product/src/scripts/migration-down.ts @@ -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() diff --git a/packages/product/src/scripts/migration-up.ts b/packages/product/src/scripts/migration-up.ts index 06c72e7ac9..aefa5cb240 100644 --- a/packages/product/src/scripts/migration-up.ts +++ b/packages/product/src/scripts/migration-up.ts @@ -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() diff --git a/packages/product/src/scripts/seed.ts b/packages/product/src/scripts/seed.ts index ec29d7605c..731a9568f4 100644 --- a/packages/product/src/scripts/seed.ts +++ b/packages/product/src/scripts/seed.ts @@ -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 { diff --git a/packages/product/src/utils/index.ts b/packages/product/src/utils/index.ts index f9e23b9cd0..8188a4c10e 100644 --- a/packages/product/src/utils/index.ts +++ b/packages/product/src/utils/index.ts @@ -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 } diff --git a/packages/product/src/utils/soft-deletable.ts b/packages/product/src/utils/soft-deletable.ts deleted file mode 100644 index b2a830f7a9..0000000000 --- a/packages/product/src/utils/soft-deletable.ts +++ /dev/null @@ -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, - }) -} diff --git a/packages/types/src/dal/repository-service.ts b/packages/types/src/dal/repository-service.ts index 062b802a62..57fd001333 100644 --- a/packages/types/src/dal/repository-service.ts +++ b/packages/types/src/dal/repository-service.ts @@ -35,13 +35,9 @@ export interface RepositoryService extends BaseRepositoryService { context?: Context ): Promise<[T[], number]> - // Only required for some repositories - upsert?(data: any, context?: Context): Promise - create(data: unknown[], context?: Context): Promise - // TODO: remove optionality when all the other repositories have an update - update?(data: unknown[], context?: Context): Promise + update(data: unknown[], context?: Context): Promise delete(ids: string[], context?: Context): Promise @@ -50,7 +46,8 @@ export interface RepositoryService extends BaseRepositoryService { restore(ids: string[], context?: Context): Promise } -export interface TreeRepositoryService extends BaseRepositoryService { +export interface TreeRepositoryService + extends BaseRepositoryService { find( options?: FindOptions, transformOptions?: RepositoryTransformOptions, diff --git a/packages/utils/src/bundles.ts b/packages/utils/src/bundles.ts index 1736971518..d47397631c 100644 --- a/packages/utils/src/bundles.ts +++ b/packages/utils/src/bundles.ts @@ -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" diff --git a/packages/utils/src/common/dal.ts b/packages/utils/src/common/dal.ts deleted file mode 100644 index 1a2a95f887..0000000000 --- a/packages/utils/src/common/dal.ts +++ /dev/null @@ -1 +0,0 @@ -export const SoftDeletableFilterKey = "softDeletable" diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 631aecae41..2d949d143c 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -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" diff --git a/packages/utils/src/dal/index.ts b/packages/utils/src/dal/index.ts new file mode 100644 index 0000000000..306a361663 --- /dev/null +++ b/packages/utils/src/dal/index.ts @@ -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" diff --git a/packages/product/src/utils/create-connection.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-create-connection.ts similarity index 80% rename from packages/product/src/utils/create-connection.ts rename to packages/utils/src/dal/mikro-orm/mikro-orm-create-connection.ts index f879d274f4..6dc92c0bbb 100644 --- a/packages/product/src/utils/create-connection.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-create-connection.ts @@ -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({ + const orm = await MikroORM.init({ discovery: { disableDynamicFileAccess: true }, entities, debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false, diff --git a/packages/product/src/repositories/base.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts similarity index 52% rename from packages/product/src/repositories/base.ts rename to packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 36ff06c197..553db4fc63 100644 --- a/packages/product/src/repositories/base.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -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( - this: any, - task: (transactionManager: unknown) => Promise, - { - transaction, - isolationLevel, - enableNestedTransactions = false, - }: { - isolationLevel?: string - transaction?: TManager - enableNestedTransactions?: boolean - } = {} -): Promise { - // 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 ( - 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 = ( - data: any, - options?: any -): Promise => { - options ??= {} - const result = serialize(data, options) - return result as unknown as Promise -} - -// TODO: move to utils package -class AbstractBase { - protected readonly manager_: SqlEntityManager +class MikroOrmBase { + protected readonly manager_: any protected constructor({ manager }) { this.manager_ = manager @@ -115,29 +29,26 @@ class AbstractBase { async transaction( task: (transactionManager: TManager) => Promise, - { - transaction, - isolationLevel, - enableNestedTransactions = false, - }: { + options: { isolationLevel?: string enableNestedTransactions?: boolean transaction?: TManager } = {} ): Promise { - return await transactionWrapper.apply(this, arguments) + // @ts-ignore + return await transactionWrapper.bind(this)(task, options) } async serialize( data: any, options?: any ): Promise { - return await serializer(data, options) + return await mikroOrmSerializer(data, options) } } -export abstract class AbstractBaseRepository - extends AbstractBase +export abstract class MikroOrmAbstractBaseRepository + extends MikroOrmBase implements DAL.RepositoryService { abstract find(options?: DAL.FindOptions, context?: Context) @@ -149,6 +60,10 @@ export abstract class AbstractBaseRepository abstract create(data: unknown[], context?: Context): Promise + update(data: unknown[], context?: Context): Promise { + throw new Error("Method not implemented.") + } + abstract delete(ids: string[], context?: Context): Promise @InjectTransactionManager() @@ -160,11 +75,7 @@ export abstract class AbstractBaseRepository 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 async restore( ids: string[], @MedusaContext() - { transactionManager: manager }: Context = {} + { transactionManager: manager }: Context = {} ): Promise { const query = buildQuery( { id: { $in: ids } }, @@ -184,19 +95,14 @@ export abstract class AbstractBaseRepository 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 - extends AbstractBase +export abstract class MikroOrmAbstractTreeRepositoryBase + extends MikroOrmBase implements DAL.TreeRepositoryService { protected constructor({ manager }) { @@ -221,13 +127,15 @@ export abstract class AbstractTreeRepositoryBase abstract delete(id: string, context?: Context): Promise } -// 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 { + throw new Error("Method not implemented.") + } + delete(ids: string[], context?: Context): Promise { 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) diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-soft-deletable-filter.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-soft-deletable-filter.ts new file mode 100644 index 0000000000..3b84fbf4fd --- /dev/null +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-soft-deletable-filter.ts @@ -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, +} diff --git a/packages/utils/src/dal/repository.ts b/packages/utils/src/dal/repository.ts new file mode 100644 index 0000000000..bcd213f86f --- /dev/null +++ b/packages/utils/src/dal/repository.ts @@ -0,0 +1,95 @@ +import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types" +import { MedusaContext } from "../decorators" + +class AbstractBase { + protected readonly manager_: any + + protected constructor({ manager }) { + this.manager_ = manager + } + + getActiveManager( + @MedusaContext() + { transactionManager, manager }: Context = {} + ): TManager { + return (transactionManager ?? manager ?? this.manager_) as TManager + } + + async transaction( + task: (transactionManager: TManager) => Promise, + { + transaction, + isolationLevel, + enableNestedTransactions = false, + }: { + isolationLevel?: string + enableNestedTransactions?: boolean + transaction?: TManager + } = {} + ): Promise { + // @ts-ignore + return await transactionWrapper.apply(this, arguments) + } +} + +export abstract class AbstractBaseRepository + extends AbstractBase + implements DAL.RepositoryService +{ + abstract find(options?: DAL.FindOptions, context?: Context) + + abstract findAndCount( + options?: DAL.FindOptions, + context?: Context + ): Promise<[T[], number]> + + abstract create(data: unknown[], context?: Context): Promise + + abstract update(data: unknown[], context?: Context): Promise + + abstract delete(ids: string[], context?: Context): Promise + + abstract softDelete(ids: string[], context?: Context): Promise + + abstract restore(ids: string[], context?: Context): Promise + + abstract getFreshManager(): TManager + + abstract serialize( + data: any, + options?: any + ): Promise +} + +export abstract class AbstractTreeRepositoryBase + extends AbstractBase + implements DAL.TreeRepositoryService +{ + protected constructor({ manager }) { + // @ts-ignore + super(...arguments) + } + + abstract find( + options?: DAL.FindOptions, + transformOptions?: RepositoryTransformOptions, + context?: Context + ) + + abstract findAndCount( + options?: DAL.FindOptions, + transformOptions?: RepositoryTransformOptions, + context?: Context + ): Promise<[T[], number]> + + abstract create(data: unknown, context?: Context): Promise + + abstract delete(id: string, context?: Context): Promise + + abstract getFreshManager(): TManager + + abstract serialize( + data: any, + options?: any + ): Promise +} diff --git a/packages/utils/src/dal/utils.ts b/packages/utils/src/dal/utils.ts new file mode 100644 index 0000000000..2590f12d22 --- /dev/null +++ b/packages/utils/src/dal/utils.ts @@ -0,0 +1,86 @@ +import { SoftDeletableFilterKey } from "../dal" + +export async function transactionWrapper( + this: any, + task: (transactionManager: unknown) => Promise, + { + transaction, + isolationLevel, + enableNestedTransactions = false, + }: { + isolationLevel?: string + transaction?: TManager + enableNestedTransactions?: boolean + } = {} +): Promise { + // 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 ( + data: any, + options?: any +): Promise => { + options ??= {} + const { serialize } = await import("@mikro-orm/core") + const result = serialize(data, options) + return result as unknown as Promise +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 6cd1081af6..79419c7344 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -5,3 +5,4 @@ export * from "./decorators" export * from "./event-bus" export * from "./search" export * from "./modules-sdk" +export * from "./dal" diff --git a/packages/utils/src/modules-sdk/build-query.ts b/packages/utils/src/modules-sdk/build-query.ts index 83971928c8..fcb220a05f 100644 --- a/packages/utils/src/modules-sdk/build-query.ts +++ b/packages/utils/src/modules-sdk/build-query.ts @@ -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( filters: Record = {},