chore(): start moving some packages to the core directory (#7215)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
export * from "./mikro-orm/big-number-field"
|
||||
export * from "./mikro-orm/mikro-orm-create-connection"
|
||||
export * from "./mikro-orm/mikro-orm-free-text-search-filter"
|
||||
export * from "./mikro-orm/mikro-orm-repository"
|
||||
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
|
||||
export * from "./mikro-orm/mikro-orm-serializer"
|
||||
export * from "./mikro-orm/utils"
|
||||
export * from "./mikro-orm/decorators/searchable"
|
||||
export * from "./repositories"
|
||||
export * from "./utils"
|
||||
@@ -0,0 +1,351 @@
|
||||
import {
|
||||
Collection,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { Searchable } from "../decorators/searchable"
|
||||
|
||||
// Circular dependency one level
|
||||
@Entity()
|
||||
class RecursiveEntity1 {
|
||||
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@OneToMany(() => RecursiveEntity2, (entity2) => entity2.entity1, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity2 = new Collection<RecursiveEntity2>(this)
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class RecursiveEntity2 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: RecursiveEntity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => RecursiveEntity1, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity1: RecursiveEntity1
|
||||
}
|
||||
|
||||
// No circular dependency
|
||||
@Entity()
|
||||
class Entity1 {
|
||||
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@OneToMany(() => Entity2, (entity2) => entity2.entity1, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity2 = new Collection<Entity2>(this)
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class Entity2 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: Entity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
this.entity1_id = props.entity1.id
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => Entity1, { mapToPk: true })
|
||||
entity1_id: string
|
||||
|
||||
@ManyToOne(() => Entity1, { persist: false })
|
||||
entity1: Entity1
|
||||
}
|
||||
|
||||
// Circular dependency deep level
|
||||
|
||||
@Entity()
|
||||
class DeepRecursiveEntity1 {
|
||||
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@OneToMany(() => DeepRecursiveEntity2, (entity2) => entity2.entity1, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity2 = new Collection<DeepRecursiveEntity2>(this)
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class DeepRecursiveEntity2 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: DeepRecursiveEntity1
|
||||
entity3: DeepRecursiveEntity3
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity3 = props.entity3
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => DeepRecursiveEntity1)
|
||||
entity1: DeepRecursiveEntity1
|
||||
|
||||
@ManyToOne(() => DeepRecursiveEntity3, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity3: DeepRecursiveEntity3
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class DeepRecursiveEntity3 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: DeepRecursiveEntity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => DeepRecursiveEntity1, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity1: DeepRecursiveEntity1
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class DeepRecursiveEntity4 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: DeepRecursiveEntity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => DeepRecursiveEntity1)
|
||||
entity1: DeepRecursiveEntity1
|
||||
}
|
||||
|
||||
// Internal circular dependency
|
||||
|
||||
@Entity()
|
||||
class InternalCircularDependencyEntity1 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
parent?: InternalCircularDependencyEntity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
|
||||
if (props.parent) {
|
||||
this.parent = props.parent
|
||||
}
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@OneToMany(
|
||||
() => InternalCircularDependencyEntity1,
|
||||
(entity) => entity.parent,
|
||||
{
|
||||
cascade: ["soft-remove"] as any,
|
||||
}
|
||||
)
|
||||
children = new Collection<InternalCircularDependencyEntity1>(this)
|
||||
|
||||
@ManyToOne(() => InternalCircularDependencyEntity1)
|
||||
parent: InternalCircularDependencyEntity1
|
||||
}
|
||||
|
||||
// With un decorated prop
|
||||
|
||||
@Entity()
|
||||
class Entity1WithUnDecoratedProp {
|
||||
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
}
|
||||
|
||||
unknownProp: string
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@OneToMany(() => Entity2WithUnDecoratedProp, (entity2) => entity2.entity1, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
entity2 = new Collection<Entity2WithUnDecoratedProp>(this)
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class Entity2WithUnDecoratedProp {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: Entity1WithUnDecoratedProp
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
this.entity1_id = props.entity1.id
|
||||
}
|
||||
|
||||
unknownProp: string
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => Entity1WithUnDecoratedProp, { mapToPk: true })
|
||||
entity1_id: string
|
||||
|
||||
@ManyToOne(() => Entity1WithUnDecoratedProp, { persist: false })
|
||||
entity1: Entity1WithUnDecoratedProp
|
||||
}
|
||||
|
||||
// Searchable fields
|
||||
|
||||
@Entity()
|
||||
class SearchableEntity1 {
|
||||
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@Searchable()
|
||||
@Property()
|
||||
searchableField: string
|
||||
|
||||
@Searchable()
|
||||
@OneToMany(() => SearchableEntity2, (entity2) => entity2.entity1)
|
||||
entity2 = new Collection<SearchableEntity2>(this)
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class SearchableEntity2 {
|
||||
constructor(props: {
|
||||
id: string
|
||||
deleted_at: Date | null
|
||||
entity1: SearchableEntity1
|
||||
}) {
|
||||
this.id = props.id
|
||||
this.deleted_at = props.deleted_at
|
||||
this.entity1 = props.entity1
|
||||
this.entity1_id = props.entity1.id
|
||||
}
|
||||
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
deleted_at: Date | null
|
||||
|
||||
@Searchable()
|
||||
@Property()
|
||||
searchableField: string
|
||||
|
||||
@ManyToOne(() => SearchableEntity1, { mapToPk: true })
|
||||
entity1_id: string
|
||||
|
||||
@ManyToOne(() => SearchableEntity1, { persist: false })
|
||||
entity1: SearchableEntity1
|
||||
}
|
||||
|
||||
export {
|
||||
RecursiveEntity1,
|
||||
RecursiveEntity2,
|
||||
Entity1,
|
||||
Entity2,
|
||||
DeepRecursiveEntity1,
|
||||
DeepRecursiveEntity2,
|
||||
DeepRecursiveEntity3,
|
||||
DeepRecursiveEntity4,
|
||||
InternalCircularDependencyEntity1,
|
||||
Entity1WithUnDecoratedProp,
|
||||
Entity2WithUnDecoratedProp,
|
||||
SearchableEntity1,
|
||||
SearchableEntity2,
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { BigNumberRawValue } from "@medusajs/types"
|
||||
import { Entity, MikroORM, PrimaryKey } from "@mikro-orm/core"
|
||||
import { BigNumber } from "../../../totals/big-number"
|
||||
import { MikroOrmBigNumberProperty } from "../big-number-field"
|
||||
|
||||
@Entity()
|
||||
class TestAmount {
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@MikroOrmBigNumberProperty()
|
||||
amount: BigNumber | number
|
||||
|
||||
raw_amount: BigNumberRawValue
|
||||
|
||||
@MikroOrmBigNumberProperty({ nullable: true })
|
||||
nullable_amount: BigNumber | number | null = null
|
||||
|
||||
raw_nullable_amount: BigNumberRawValue | null = null
|
||||
}
|
||||
|
||||
describe("@MikroOrmBigNumberProperty", () => {
|
||||
let orm!: MikroORM
|
||||
|
||||
beforeEach(async () => {
|
||||
orm = await MikroORM.init({
|
||||
entities: [TestAmount],
|
||||
dbName: "test",
|
||||
type: "postgresql",
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await orm.close()
|
||||
})
|
||||
|
||||
it("should correctly assign and update BigNumber values", () => {
|
||||
const testAmount = new TestAmount()
|
||||
|
||||
expect(testAmount.amount).toBeUndefined()
|
||||
expect(testAmount.raw_amount).toBeUndefined()
|
||||
|
||||
testAmount.amount = 100
|
||||
|
||||
expect(testAmount.amount).toEqual(100)
|
||||
expect(testAmount.raw_amount).toEqual({
|
||||
value: "100",
|
||||
precision: 20,
|
||||
})
|
||||
|
||||
try {
|
||||
;(testAmount as any).amount = null
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
"Invalid BigNumber value: null. Should be one of: string, number, BigNumber (bignumber.js), BigNumberRawValue"
|
||||
)
|
||||
}
|
||||
|
||||
testAmount.nullable_amount = null
|
||||
expect(testAmount.nullable_amount).toEqual(null)
|
||||
// Update the amount
|
||||
|
||||
testAmount.amount = 200
|
||||
|
||||
expect(testAmount.amount).toEqual(200)
|
||||
expect(testAmount.raw_amount).toEqual({
|
||||
value: "200",
|
||||
precision: 20,
|
||||
})
|
||||
|
||||
// Update with big number
|
||||
|
||||
testAmount.amount = new BigNumber(300, { precision: 5 })
|
||||
|
||||
expect(testAmount.amount).toEqual(300)
|
||||
expect(testAmount.raw_amount).toEqual({ value: "300", precision: 5 })
|
||||
})
|
||||
})
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { MikroORM } from "@mikro-orm/core"
|
||||
import { SearchableEntity1, SearchableEntity2 } from "../__fixtures__/utils"
|
||||
import { mikroOrmFreeTextSearchFilterOptionsFactory } from "../mikro-orm-free-text-search-filter"
|
||||
|
||||
describe("mikroOrmFreeTextSearchFilterOptionsFactory", () => {
|
||||
let orm
|
||||
|
||||
beforeEach(async () => {
|
||||
orm = await MikroORM.init({
|
||||
entities: [SearchableEntity1, SearchableEntity2],
|
||||
dbName: "test",
|
||||
type: "postgresql",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return a filter function that filters entities based on the free text search value", async () => {
|
||||
const entityManager = orm.em.fork()
|
||||
const freeTextSearchValue = "search"
|
||||
|
||||
const models = [SearchableEntity1, SearchableEntity2]
|
||||
|
||||
let filterConstraints = mikroOrmFreeTextSearchFilterOptionsFactory(
|
||||
models
|
||||
).cond(
|
||||
{
|
||||
value: freeTextSearchValue,
|
||||
fromEntity: SearchableEntity1.name,
|
||||
},
|
||||
"read",
|
||||
entityManager
|
||||
)
|
||||
|
||||
expect(filterConstraints).toEqual({
|
||||
$or: [
|
||||
{
|
||||
searchableField: {
|
||||
$ilike: `%${freeTextSearchValue}%`,
|
||||
},
|
||||
},
|
||||
{
|
||||
entity2: {
|
||||
$or: [
|
||||
{
|
||||
searchableField: {
|
||||
$ilike: `%${freeTextSearchValue}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
filterConstraints = mikroOrmFreeTextSearchFilterOptionsFactory(models).cond(
|
||||
{
|
||||
value: freeTextSearchValue,
|
||||
fromEntity: SearchableEntity2.name,
|
||||
},
|
||||
"read",
|
||||
entityManager
|
||||
)
|
||||
|
||||
expect(filterConstraints).toEqual({
|
||||
$or: [
|
||||
{
|
||||
searchableField: {
|
||||
$ilike: `%${freeTextSearchValue}%`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import { MikroORM } from "@mikro-orm/core"
|
||||
import {
|
||||
Entity1WithUnDecoratedProp,
|
||||
Entity2WithUnDecoratedProp,
|
||||
} from "../__fixtures__/utils"
|
||||
import { mikroOrmSerializer } from "../mikro-orm-serializer"
|
||||
|
||||
describe("mikroOrmSerializer", () => {
|
||||
beforeEach(async () => {
|
||||
await MikroORM.init({
|
||||
entities: [Entity1WithUnDecoratedProp, Entity2WithUnDecoratedProp],
|
||||
dbName: "test",
|
||||
type: "postgresql",
|
||||
})
|
||||
})
|
||||
|
||||
it("should serialize an entity", async () => {
|
||||
const entity1 = new Entity1WithUnDecoratedProp({
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
})
|
||||
entity1.unknownProp = "calculated"
|
||||
|
||||
const entity2 = new Entity2WithUnDecoratedProp({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
entity1.entity2.add(entity2)
|
||||
|
||||
const serialized = await mikroOrmSerializer(entity1, {
|
||||
preventCircularRef: false,
|
||||
})
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
unknownProp: "calculated",
|
||||
entity2: [
|
||||
{
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: {
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
unknownProp: "calculated",
|
||||
},
|
||||
entity1_id: "1",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("should serialize an array of entities", async () => {
|
||||
const entity1 = new Entity1WithUnDecoratedProp({
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
})
|
||||
entity1.unknownProp = "calculated"
|
||||
|
||||
const entity2 = new Entity2WithUnDecoratedProp({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
entity1.entity2.add(entity2)
|
||||
|
||||
const serialized = await mikroOrmSerializer([entity1, entity1], {
|
||||
preventCircularRef: false,
|
||||
})
|
||||
|
||||
const expectation = {
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
unknownProp: "calculated",
|
||||
entity2: [
|
||||
{
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: {
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
unknownProp: "calculated",
|
||||
},
|
||||
entity1_id: "1",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
expect(serialized).toEqual([expectation, expectation])
|
||||
})
|
||||
|
||||
it("should serialize an entity preventing circular relation reference", async () => {
|
||||
const entity1 = new Entity1WithUnDecoratedProp({
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
})
|
||||
entity1.unknownProp = "calculated"
|
||||
|
||||
const entity2 = new Entity2WithUnDecoratedProp({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
entity1.entity2.add(entity2)
|
||||
|
||||
const serialized = await mikroOrmSerializer(entity1)
|
||||
|
||||
expect(serialized).toEqual({
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
unknownProp: "calculated",
|
||||
entity2: [
|
||||
{
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1_id: "1",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { TSMigrationGenerator } from "../mikro-orm-create-connection"
|
||||
|
||||
function unwrapSql(sql: string) {
|
||||
return sql.match(/this.addSql\('(.*?)'\)/)?.[1]
|
||||
}
|
||||
|
||||
describe("TSMigrationGenerator", () => {
|
||||
it('should add "if not exists" to "create table" statements', () => {
|
||||
const sql = "create table my_table (id int)"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("create table if not exists my_table (id int)")
|
||||
})
|
||||
|
||||
it('should add "if exists" to "alter table" statements as well as to the add column', () => {
|
||||
const sql = "alter table my_table add column name varchar(100)"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe(
|
||||
"alter table if exists my_table add column if not exists name varchar(100)"
|
||||
)
|
||||
})
|
||||
|
||||
it('should add "if exists" to "alter table" statements as well as to the drop column', () => {
|
||||
const sql = "alter table my_table drop column name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe(
|
||||
"alter table if exists my_table drop column if exists name"
|
||||
)
|
||||
})
|
||||
|
||||
it('should add "if exists" to "alter table" statements as well as to the alter column', () => {
|
||||
const sql = "alter table my_table alter column name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe(
|
||||
"alter table if exists my_table alter column if exists name"
|
||||
)
|
||||
})
|
||||
|
||||
it('should add "if not exists" to "create index" statements', () => {
|
||||
const sql = "create index idx_name on my_table(name)"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("create index if not exists idx_name on my_table(name)")
|
||||
})
|
||||
|
||||
it('should add "if exists" to "drop index" statements', () => {
|
||||
const sql = "drop index idx_name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("drop index if exists idx_name")
|
||||
})
|
||||
|
||||
it('should add "if not exists" to "create unique index" statements', () => {
|
||||
const sql = "create unique index idx_unique_name on my_table(name)"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe(
|
||||
"create unique index if not exists idx_unique_name on my_table(name)"
|
||||
)
|
||||
})
|
||||
|
||||
it('should add "if exists" to "drop unique index" statements', () => {
|
||||
const sql = "drop unique index idx_unique_name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("drop unique index if exists idx_unique_name")
|
||||
})
|
||||
|
||||
it('should add "if not exists" to "add column" statements', () => {
|
||||
const sql = "add column name varchar(100)"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("add column if not exists name varchar(100)")
|
||||
})
|
||||
|
||||
it('should add "if exists" to "drop column" statements', () => {
|
||||
const sql = "drop column name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("drop column if exists name")
|
||||
})
|
||||
|
||||
it('should add "if exists" to "alter column" statements', () => {
|
||||
const sql = "alter column name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("alter column if exists name")
|
||||
})
|
||||
|
||||
it('should add "if exists" to "drop constraint" statements', () => {
|
||||
const sql = "drop constraint fk_name"
|
||||
const result = unwrapSql(
|
||||
TSMigrationGenerator.prototype.createStatement(sql, 0)
|
||||
)
|
||||
expect(result).toBe("drop constraint if exists fk_name")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
import { mikroOrmUpdateDeletedAtRecursively } from "../utils"
|
||||
import { MikroORM } from "@mikro-orm/core"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
DeepRecursiveEntity1,
|
||||
DeepRecursiveEntity2,
|
||||
DeepRecursiveEntity3,
|
||||
DeepRecursiveEntity4,
|
||||
Entity1,
|
||||
Entity2,
|
||||
InternalCircularDependencyEntity1,
|
||||
RecursiveEntity1,
|
||||
RecursiveEntity2,
|
||||
} from "../__fixtures__/utils"
|
||||
|
||||
jest.mock("@mikro-orm/core", () => ({
|
||||
...jest.requireActual("@mikro-orm/core"),
|
||||
wrap: jest.fn().mockImplementation((entity) => ({
|
||||
...entity,
|
||||
init: jest.fn().mockResolvedValue(entity),
|
||||
__helper: {
|
||||
isInitialized: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
})),
|
||||
}))
|
||||
|
||||
describe("mikroOrmUpdateDeletedAtRecursively", () => {
|
||||
describe("using circular cascading", () => {
|
||||
let orm!: MikroORM
|
||||
|
||||
beforeEach(async () => {
|
||||
orm = await MikroORM.init({
|
||||
entities: [
|
||||
Entity1,
|
||||
Entity2,
|
||||
RecursiveEntity1,
|
||||
RecursiveEntity2,
|
||||
DeepRecursiveEntity1,
|
||||
DeepRecursiveEntity2,
|
||||
DeepRecursiveEntity3,
|
||||
DeepRecursiveEntity4,
|
||||
InternalCircularDependencyEntity1,
|
||||
],
|
||||
dbName: "test",
|
||||
type: "postgresql",
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await orm.close()
|
||||
})
|
||||
|
||||
it("should successfully mark the entities deleted_at recursively", async () => {
|
||||
const manager = orm.em.fork() as SqlEntityManager
|
||||
const entity1 = new Entity1({ id: "1", deleted_at: null })
|
||||
const entity2 = new Entity2({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
entity1.entity2.add(entity2)
|
||||
|
||||
const deletedAt = new Date()
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, [entity1], deletedAt)
|
||||
|
||||
expect(entity1.deleted_at).toEqual(deletedAt)
|
||||
expect(entity2.deleted_at).toEqual(deletedAt)
|
||||
})
|
||||
|
||||
it("should successfully mark the entities deleted_at recursively with internal parent/child relation", async () => {
|
||||
const manager = orm.em.fork() as SqlEntityManager
|
||||
const entity1 = new InternalCircularDependencyEntity1({
|
||||
id: "1",
|
||||
deleted_at: null,
|
||||
})
|
||||
|
||||
const childEntity1 = new InternalCircularDependencyEntity1({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
parent: entity1,
|
||||
})
|
||||
|
||||
const deletedAt = new Date()
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, [entity1], deletedAt)
|
||||
|
||||
expect(entity1.deleted_at).toEqual(deletedAt)
|
||||
expect(childEntity1.deleted_at).toEqual(deletedAt)
|
||||
})
|
||||
|
||||
it("should throw an error when a circular dependency is detected", async () => {
|
||||
const manager = orm.em.fork() as SqlEntityManager
|
||||
const entity1 = new RecursiveEntity1({ id: "1", deleted_at: null })
|
||||
const entity2 = new RecursiveEntity2({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
|
||||
await expect(
|
||||
mikroOrmUpdateDeletedAtRecursively(manager, [entity2], new Date())
|
||||
).rejects.toThrow(
|
||||
"Unable to soft delete the entity1. Circular dependency detected: RecursiveEntity2 -> entity1 -> RecursiveEntity1 -> entity2 -> RecursiveEntity2"
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error when a circular dependency is detected even at a deeper level", async () => {
|
||||
const manager = orm.em.fork() as SqlEntityManager
|
||||
const entity1 = new DeepRecursiveEntity1({ id: "1", deleted_at: null })
|
||||
const entity3 = new DeepRecursiveEntity3({
|
||||
id: "3",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
const entity2 = new DeepRecursiveEntity2({
|
||||
id: "2",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
entity3: entity3,
|
||||
})
|
||||
const entity4 = new DeepRecursiveEntity4({
|
||||
id: "4",
|
||||
deleted_at: null,
|
||||
entity1: entity1,
|
||||
})
|
||||
|
||||
await expect(
|
||||
mikroOrmUpdateDeletedAtRecursively(manager, [entity1], new Date())
|
||||
).rejects.toThrow(
|
||||
"Unable to soft delete the entity2. Circular dependency detected: DeepRecursiveEntity1 -> entity2 -> DeepRecursiveEntity2 -> entity3 -> DeepRecursiveEntity3 -> entity1 -> DeepRecursiveEntity1"
|
||||
)
|
||||
|
||||
await expect(
|
||||
mikroOrmUpdateDeletedAtRecursively(manager, [entity2], new Date())
|
||||
).rejects.toThrow(
|
||||
"Unable to soft delete the entity3. Circular dependency detected: DeepRecursiveEntity2 -> entity3 -> DeepRecursiveEntity3 -> entity1 -> DeepRecursiveEntity1 -> entity2 -> DeepRecursiveEntity2"
|
||||
)
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, [entity4], new Date())
|
||||
expect(entity4.deleted_at).not.toBeNull()
|
||||
expect(entity1.deleted_at).toBeNull()
|
||||
expect(entity2.deleted_at).toBeNull()
|
||||
expect(entity3.deleted_at).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Property } from "@mikro-orm/core"
|
||||
import { isPresent, trimZeros } from "../../common"
|
||||
import { BigNumber } from "../../totals/big-number"
|
||||
import { BigNumberInput } from "@medusajs/types"
|
||||
|
||||
export function MikroOrmBigNumberProperty(
|
||||
options: Parameters<typeof Property>[0] & {
|
||||
rawColumnName?: string
|
||||
} = {}
|
||||
) {
|
||||
return function (target: any, columnName: string) {
|
||||
const rawColumnName = options.rawColumnName ?? `raw_${columnName}`
|
||||
|
||||
Object.defineProperty(target, columnName, {
|
||||
get() {
|
||||
let value = this.__helper?.__data?.[columnName]
|
||||
|
||||
if (!value && this[rawColumnName]) {
|
||||
value = new BigNumber(this[rawColumnName].value, {
|
||||
precision: this[rawColumnName].precision,
|
||||
}).numeric
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
set(value: BigNumberInput) {
|
||||
if (options?.nullable && !isPresent(value)) {
|
||||
this.__helper.__data[columnName] = null
|
||||
this.__helper.__data[rawColumnName]
|
||||
this[rawColumnName] = null
|
||||
} else {
|
||||
let bigNumber: BigNumber
|
||||
|
||||
if (value instanceof BigNumber) {
|
||||
bigNumber = value
|
||||
} else if (this[rawColumnName]) {
|
||||
const precision = this[rawColumnName].precision
|
||||
bigNumber = new BigNumber(value, {
|
||||
precision,
|
||||
})
|
||||
} else {
|
||||
bigNumber = new BigNumber(value)
|
||||
}
|
||||
|
||||
const raw = bigNumber.raw!
|
||||
raw.value = trimZeros(raw.value as string)
|
||||
|
||||
this.__helper.__data[columnName] = bigNumber.numeric
|
||||
this.__helper.__data[rawColumnName] = raw
|
||||
|
||||
this[rawColumnName] = raw
|
||||
}
|
||||
|
||||
// This is custom code to keep track of which fields are bignumber, as well as their data
|
||||
if (!this.__helper.__bignumberdata) {
|
||||
this.__helper.__bignumberdata = {}
|
||||
}
|
||||
|
||||
this.__helper.__bignumberdata[columnName] =
|
||||
this.__helper.__data[columnName]
|
||||
this.__helper.__bignumberdata[rawColumnName] =
|
||||
this.__helper.__data[rawColumnName]
|
||||
this.__helper.__touched = !this.__helper.hydrator.isRunning()
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
Property({
|
||||
type: "any",
|
||||
columnType: "numeric",
|
||||
trackChanges: false,
|
||||
...options,
|
||||
})(target, columnName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
ForeignKeyConstraintViolationException,
|
||||
InvalidFieldNameException,
|
||||
NotFoundError,
|
||||
NotNullConstraintViolationException,
|
||||
UniqueConstraintViolationException,
|
||||
} from "@mikro-orm/core"
|
||||
import { MedusaError, upperCaseFirst } from "../../common"
|
||||
|
||||
export const dbErrorMapper = (err: Error) => {
|
||||
if (err instanceof NotFoundError) {
|
||||
console.log(err)
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, err.message)
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof UniqueConstraintViolationException ||
|
||||
(err as any).code === "23505"
|
||||
) {
|
||||
const info = getConstraintInfo(err)
|
||||
if (!info) {
|
||||
throw err
|
||||
}
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`${upperCaseFirst(info.table.split("_").join(" "))} with ${info.keys
|
||||
.map((key, i) => `${key}: ${info.values[i]}`)
|
||||
.join(", ")} already exists.`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof NotNullConstraintViolationException ||
|
||||
(err as any).code === "23502"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot set field '${(err as any).column}' of ${upperCaseFirst(
|
||||
(err as any).table.split("_").join(" ")
|
||||
)} to null`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof InvalidFieldNameException ||
|
||||
(err as any).code === "42703"
|
||||
) {
|
||||
const userFriendlyMessage = err.message.match(/(column.*)/)?.[0]
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
userFriendlyMessage ?? err.message
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
err instanceof ForeignKeyConstraintViolationException ||
|
||||
(err as any).code === "23503"
|
||||
) {
|
||||
const info = getConstraintInfo(err)
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`You tried to set relationship ${info?.keys.map(
|
||||
(key, i) => `${key}: ${info.values[i]}`
|
||||
)}, but such entity does not exist`
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
const getConstraintInfo = (err: any) => {
|
||||
const detail = err.detail as string
|
||||
if (!detail) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [keys, values] = detail.match(/\([^\(.]*\)/g) || []
|
||||
|
||||
if (!keys || !values) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
table: err.table.split("_").join(" "),
|
||||
keys: keys
|
||||
.substring(1, keys.length - 1)
|
||||
.split(",")
|
||||
.map((k) => k.trim()),
|
||||
values: values
|
||||
.substring(1, values.length - 1)
|
||||
.split(",")
|
||||
.map((v) => v.trim()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { MetadataStorage } from "@mikro-orm/core"
|
||||
|
||||
export function Searchable() {
|
||||
return function (target, propertyName) {
|
||||
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor)
|
||||
const prop = meta.properties[propertyName] || {}
|
||||
prop["searchable"] = true
|
||||
meta.properties[prop.name] = prop
|
||||
}
|
||||
}
|
||||
+900
@@ -0,0 +1,900 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
EntityManager,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
MikroORM,
|
||||
OneToMany,
|
||||
OnInit,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
wrap,
|
||||
} from "@mikro-orm/core"
|
||||
import { mikroOrmBaseRepositoryFactory } from "../../mikro-orm-repository"
|
||||
import { dropDatabase } from "pg-god"
|
||||
import { MikroOrmBigNumberProperty } from "../../big-number-field"
|
||||
import BigNumber from "bignumber.js"
|
||||
import { BigNumberRawValue } from "@medusajs/types"
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD
|
||||
const DB_NAME = "mikroorm-integration-1"
|
||||
|
||||
const pgGodCredentials = {
|
||||
user: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
host: DB_HOST,
|
||||
}
|
||||
|
||||
export function getDatabaseURL(): string {
|
||||
return `postgres://${DB_USERNAME}${
|
||||
DB_PASSWORD ? `:${DB_PASSWORD}` : ""
|
||||
}@${DB_HOST}/${DB_NAME}`
|
||||
}
|
||||
|
||||
jest.setTimeout(300000)
|
||||
@Entity()
|
||||
class Entity1 {
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
title: string
|
||||
|
||||
@MikroOrmBigNumberProperty({ nullable: true })
|
||||
amount: BigNumber | number | null
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
raw_amount: BigNumberRawValue | null
|
||||
|
||||
@Property({ nullable: true })
|
||||
deleted_at: Date | null
|
||||
|
||||
@OneToMany(() => Entity2, (entity2) => entity2.entity1)
|
||||
entity2 = new Collection<Entity2>(this)
|
||||
|
||||
@ManyToMany(() => Entity3, "entity1", {
|
||||
owner: true,
|
||||
pivotTable: "entity_1_3",
|
||||
})
|
||||
entity3 = new Collection<Entity3>(this)
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
if (!this.id) {
|
||||
this.id = Math.random().toString(36).substring(7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class Entity2 {
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Property()
|
||||
title: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToOne(() => Entity1, {
|
||||
columnType: "text",
|
||||
nullable: true,
|
||||
mapToPk: true,
|
||||
fieldName: "entity1_id",
|
||||
onDelete: "set null",
|
||||
})
|
||||
entity1_id: string
|
||||
|
||||
@ManyToOne(() => Entity1, { persist: false, nullable: true })
|
||||
entity1: Entity1 | null
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
if (!this.id) {
|
||||
this.id = Math.random().toString(36).substring(7)
|
||||
}
|
||||
|
||||
this.entity1_id ??= this.entity1?.id!
|
||||
}
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class Entity3 {
|
||||
@PrimaryKey()
|
||||
id: string
|
||||
|
||||
@Unique()
|
||||
@Property()
|
||||
title: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
deleted_at: Date | null
|
||||
|
||||
@ManyToMany(() => Entity1, (entity1) => entity1.entity3)
|
||||
entity1 = new Collection<Entity1>(this)
|
||||
|
||||
@OnInit()
|
||||
@BeforeCreate()
|
||||
onInit() {
|
||||
if (!this.id) {
|
||||
this.id = Math.random().toString(36).substring(7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Entity1Repository = mikroOrmBaseRepositoryFactory<Entity1>(Entity1)
|
||||
const Entity2Repository = mikroOrmBaseRepositoryFactory<Entity2>(Entity2)
|
||||
const Entity3Repository = mikroOrmBaseRepositoryFactory<Entity3>(Entity3)
|
||||
|
||||
describe("mikroOrmRepository", () => {
|
||||
let orm!: MikroORM
|
||||
let manager!: EntityManager
|
||||
const manager1 = () => {
|
||||
return new Entity1Repository({ manager: manager.fork() })
|
||||
}
|
||||
const manager2 = () => {
|
||||
return new Entity2Repository({ manager: manager.fork() })
|
||||
}
|
||||
const manager3 = () => {
|
||||
return new Entity3Repository({ manager: manager.fork() })
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await dropDatabase(
|
||||
{ databaseName: DB_NAME, errorIfNonExist: false },
|
||||
pgGodCredentials
|
||||
)
|
||||
|
||||
orm = await MikroORM.init({
|
||||
entities: [Entity1, Entity2],
|
||||
clientUrl: getDatabaseURL(),
|
||||
type: "postgresql",
|
||||
})
|
||||
|
||||
const generator = orm.getSchemaGenerator()
|
||||
await generator.ensureDatabase()
|
||||
await generator.createSchema()
|
||||
|
||||
manager = orm.em.fork()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const generator = orm.getSchemaGenerator()
|
||||
await generator.dropSchema()
|
||||
await orm.close(true)
|
||||
})
|
||||
|
||||
describe("upsert with replace", () => {
|
||||
it("should successfully create a flat entity", async () => {
|
||||
const entity1 = { id: "1", title: "en1", amount: 100 }
|
||||
|
||||
const resp = await manager1().upsertWithReplace([entity1])
|
||||
const listedEntities = await manager1().find()
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(wrap(listedEntities[0]).toPOJO()).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
amount: 100,
|
||||
raw_amount: { value: "100", precision: 20 },
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should successfully update a flat entity", async () => {
|
||||
const entity1 = { id: "1", title: "en1" }
|
||||
|
||||
await manager1().upsertWithReplace([entity1])
|
||||
entity1.title = "newen1"
|
||||
await manager1().upsertWithReplace([entity1])
|
||||
const listedEntities = await manager1().find()
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(wrap(listedEntities[0]).toPOJO()).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "newen1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should successfully do a partial update a flat entity", async () => {
|
||||
const entity1 = { id: "1", title: "en1" }
|
||||
|
||||
await manager1().upsertWithReplace([entity1])
|
||||
entity1.title = undefined as any
|
||||
await manager1().upsertWithReplace([entity1])
|
||||
const listedEntities = await manager1().find()
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(wrap(listedEntities[0]).toPOJO()).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw if a sub-entity is passed in a many-to-one relation", async () => {
|
||||
const entity2 = {
|
||||
id: "2",
|
||||
title: "en2",
|
||||
entity1: { title: "en1" },
|
||||
}
|
||||
|
||||
const errMsg = await manager2()
|
||||
.upsertWithReplace([entity2])
|
||||
.catch((e) => e.message)
|
||||
|
||||
expect(errMsg).toEqual(
|
||||
"Many-to-one relation entity1 must be set with an ID"
|
||||
)
|
||||
})
|
||||
|
||||
it("should successfully create the parent entity of a many-to-one", async () => {
|
||||
const entity2 = {
|
||||
id: "2",
|
||||
title: "en2",
|
||||
}
|
||||
|
||||
await manager2().upsertWithReplace([entity2], {
|
||||
relations: [],
|
||||
})
|
||||
const listedEntities = await manager2().find({
|
||||
where: { id: "2" },
|
||||
options: { populate: ["entity1"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "2",
|
||||
title: "en2",
|
||||
entity1: null,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should set an entity to parent entity of a many-to-one relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
}
|
||||
|
||||
const entity2 = {
|
||||
id: "2",
|
||||
title: "en2",
|
||||
entity1: { id: "1" },
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1])
|
||||
await manager2().upsertWithReplace([entity2])
|
||||
|
||||
const listedEntities = await manager2().find({
|
||||
where: { id: "2" },
|
||||
options: { populate: ["entity1"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(JSON.parse(JSON.stringify(listedEntities[0]))).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "2",
|
||||
title: "en2",
|
||||
entity1: expect.objectContaining({
|
||||
title: "en1",
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should successfully unset an entity of a many-to-one relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
}
|
||||
|
||||
const entity2 = {
|
||||
id: "2",
|
||||
title: "en2",
|
||||
entity1: { id: "1" },
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1])
|
||||
await manager2().upsertWithReplace([entity2])
|
||||
|
||||
entity2.entity1 = null as any
|
||||
await manager2().upsertWithReplace([entity2])
|
||||
|
||||
const listedEntities = await manager2().find({
|
||||
where: { id: "2" },
|
||||
options: { populate: ["entity1"] },
|
||||
})
|
||||
|
||||
const listedEntity1 = await manager1().find({
|
||||
where: {},
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "2",
|
||||
title: "en2",
|
||||
entity1: null,
|
||||
})
|
||||
)
|
||||
|
||||
expect(listedEntity1).toHaveLength(1)
|
||||
expect(listedEntity1[0].title).toEqual("en1")
|
||||
})
|
||||
|
||||
it("should only create the parent entity of a one-to-many if relation is not included", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity2: [{ title: "en2-1" }, { title: "en2-2" }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: [],
|
||||
})
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity2"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntities[0].entity2.getItems()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should successfully create an entity with a sub-entity one-to-many relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity2: [{ title: "en2-1" }, { title: "en2-2" }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity2"],
|
||||
})
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity2"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntities[0].entity2.getItems()).toHaveLength(2)
|
||||
expect(listedEntities[0].entity2.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en2-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en2-2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should clear the parent entity from the one-to-many relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity2: [{ title: "en2-1", entity1: null }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity2"],
|
||||
})
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity2"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntities[0].entity2.getItems()).toHaveLength(1)
|
||||
expect(listedEntities[0].entity2.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en2-1",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should only update the parent entity of a one-to-many if relation is not included", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity2: [{ title: "en2-1" }, { title: "en2-2" }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity2"],
|
||||
})
|
||||
entity1.entity2.push({ title: "en2-3" })
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: [],
|
||||
})
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity2"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntities[0].entity2.getItems()).toHaveLength(2)
|
||||
expect(listedEntities[0].entity2.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en2-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en2-2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should successfully update, create, and delete subentities an entity with a one-to-many relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity2: [
|
||||
{ id: "2", title: "en2-1" },
|
||||
{ id: "3", title: "en2-2" },
|
||||
] as any[],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity2"],
|
||||
})
|
||||
|
||||
entity1.entity2 = [{ id: "2", title: "newen2-1" }, { title: "en2-3" }]
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity2"],
|
||||
})
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity2"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntities[0].entity2.getItems()).toHaveLength(2)
|
||||
expect(listedEntities[0].entity2.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "newen2-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en2-3",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should only create the parent entity of a many-to-many if relation is not included", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [{ title: "en3-1" }, { title: "en3-2" }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: [],
|
||||
})
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity3"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntities[0].entity3.getItems()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should successfully create an entity with a sub-entity many-to-many relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [{ title: "en3-1" }, { title: "en3-2" }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
const listedEntity1 = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity3"] },
|
||||
})
|
||||
|
||||
const listedEntity3 = await manager3().find({
|
||||
where: { title: "en3-1" },
|
||||
options: { populate: ["entity1"] },
|
||||
})
|
||||
|
||||
expect(listedEntity1).toHaveLength(1)
|
||||
expect(listedEntity1[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "1",
|
||||
title: "en1",
|
||||
})
|
||||
)
|
||||
expect(listedEntity1[0].entity3.getItems()).toHaveLength(2)
|
||||
expect(listedEntity1[0].entity3.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en3-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en3-2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(listedEntity3).toHaveLength(1)
|
||||
expect(listedEntity3[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
title: "en3-1",
|
||||
})
|
||||
)
|
||||
expect(listedEntity3[0].entity1.getItems()).toHaveLength(1)
|
||||
expect(listedEntity3[0].entity1.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en1",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should only update the parent entity of a many-to-many if relation is not included", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [{ title: "en3-1" }, { title: "en3-2" }],
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
entity1.title = "newen1"
|
||||
entity1.entity3.push({ title: "en3-3" })
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: [],
|
||||
})
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity3"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0].title).toEqual("newen1")
|
||||
|
||||
expect(listedEntities[0].entity3.getItems()).toHaveLength(2)
|
||||
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en3-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en3-2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should successfully create subentities and delete pivot relationships on a many-to-many relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [
|
||||
{ id: "4", title: "en3-1" },
|
||||
{ id: "5", title: "en3-2" },
|
||||
] as any,
|
||||
}
|
||||
|
||||
let resp = await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
entity1.title = "newen1"
|
||||
entity1.entity3 = [{ id: "4", title: "newen3-1" }, { title: "en3-4" }]
|
||||
|
||||
// We don't do many-to-many updates, so id: 4 entity should remain unchanged
|
||||
resp = await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity3"] },
|
||||
})
|
||||
|
||||
const listedEntity3 = await manager3().find({
|
||||
where: {},
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0].title).toEqual("newen1")
|
||||
expect(listedEntities[0].entity3.getItems()).toHaveLength(2)
|
||||
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en3-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en3-4",
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
// Many-to-many don't get deleted afterwards, even if they were disassociated
|
||||
expect(listedEntity3).toHaveLength(3)
|
||||
})
|
||||
|
||||
it("should successfully remove relationship when an empty array is passed in a many-to-many relation", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [
|
||||
{ id: "4", title: "en3-1" },
|
||||
{ id: "5", title: "en3-2" },
|
||||
] as any,
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
entity1.title = "newen1"
|
||||
entity1.entity3 = []
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity3"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0].title).toEqual("newen1")
|
||||
expect(listedEntities[0].entity3.getItems()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should correctly handle sub-entity upserts", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [
|
||||
{ id: "4", title: "en3-1" },
|
||||
{ id: "5", title: "en3-2" },
|
||||
] as any,
|
||||
}
|
||||
|
||||
const mainEntity = await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
entity1.title = "newen1"
|
||||
entity1.entity3 = [{ id: "4", title: "newen3-1" }, { title: "en3-4" }]
|
||||
|
||||
// We don't do many-to-many updates, so id: 4 entity should remain unchanged
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
// The sub-entity upsert should happen after the main was created
|
||||
await manager2().upsertWithReplace([
|
||||
{ id: "2", title: "en2", entity1_id: mainEntity[0].id },
|
||||
])
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: { id: "1" },
|
||||
options: { populate: ["entity2", "entity3"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(1)
|
||||
expect(listedEntities[0].title).toEqual("newen1")
|
||||
expect(listedEntities[0].entity2.getItems()).toHaveLength(1)
|
||||
expect(listedEntities[0].entity2.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(listedEntities[0].entity3.getItems()).toHaveLength(2)
|
||||
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: "en3-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "en3-4",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should return the complete dependency tree as a response, with IDs populated", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity2: [{ title: "en2" }],
|
||||
entity3: [{ title: "en3-1" }, { title: "en3-2" }] as any,
|
||||
}
|
||||
|
||||
const [createResp] = await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity2", "entity3"],
|
||||
})
|
||||
createResp.title = "newen1"
|
||||
const [updateResp] = await manager1().upsertWithReplace([createResp], {
|
||||
relations: ["entity2", "entity3"],
|
||||
})
|
||||
|
||||
expect(createResp.id).toEqual("1")
|
||||
expect(createResp.title).toEqual("newen1")
|
||||
expect(createResp.entity2).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "en2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(createResp.entity3).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "en3-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "en3-2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(updateResp.id).toEqual("1")
|
||||
expect(updateResp.title).toEqual("newen1")
|
||||
expect(updateResp.entity2).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "en2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(updateResp.entity3).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "en3-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
title: "en3-2",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should correctly handle many-to-many upserts with a uniqueness constriant on a non-primary key", async () => {
|
||||
const entity1 = {
|
||||
id: "1",
|
||||
title: "en1",
|
||||
entity3: [
|
||||
{ id: "4", title: "en3-1" },
|
||||
{ id: "5", title: "en3-2" },
|
||||
] as any,
|
||||
}
|
||||
|
||||
await manager1().upsertWithReplace([entity1], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
await manager1().upsertWithReplace([{ ...entity1, id: "2" }], {
|
||||
relations: ["entity3"],
|
||||
})
|
||||
|
||||
const listedEntities = await manager1().find({
|
||||
where: {},
|
||||
options: { populate: ["entity3"] },
|
||||
})
|
||||
|
||||
expect(listedEntities).toHaveLength(2)
|
||||
expect(listedEntities[0].entity3.getItems()).toEqual(
|
||||
listedEntities[1].entity3.getItems()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("error mapping", () => {
|
||||
it("should map UniqueConstraintViolationException to MedusaError on upsertWithReplace", async () => {
|
||||
const entity3 = { title: "en3" }
|
||||
await manager3().upsertWithReplace([entity3])
|
||||
const err = await manager3()
|
||||
.upsertWithReplace([entity3])
|
||||
.catch((e) => e.message)
|
||||
|
||||
expect(err).toEqual("Entity3 with title: en3 already exists.")
|
||||
})
|
||||
|
||||
it("should map NotNullConstraintViolationException MedusaError on upsertWithReplace", async () => {
|
||||
const entity3 = { title: null }
|
||||
const err = await manager3()
|
||||
.upsertWithReplace([entity3])
|
||||
.catch((e) => e.message)
|
||||
|
||||
expect(err).toEqual("Cannot set field 'title' of Entity3 to null")
|
||||
})
|
||||
|
||||
it("should map InvalidFieldNameException MedusaError on upsertWithReplace", async () => {
|
||||
const entity3 = { othertitle: "en3" }
|
||||
const err = await manager3()
|
||||
.upsertWithReplace([entity3])
|
||||
.catch((e) => e.message)
|
||||
|
||||
expect(err).toEqual(
|
||||
'column "othertitle" of relation "entity3" does not exist'
|
||||
)
|
||||
})
|
||||
it("should map ForeignKeyConstraintViolationException MedusaError on upsertWithReplace", async () => {
|
||||
const entity2 = { title: "en2", entity1: { id: "1" } }
|
||||
const err = await manager2()
|
||||
.upsertWithReplace([entity2])
|
||||
.catch((e) => e.message)
|
||||
|
||||
expect(err).toEqual(
|
||||
"You tried to set relationship entity1_id: 1, but such entity does not exist"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
import { ModuleServiceInitializeOptions } from "@medusajs/types"
|
||||
import { TSMigrationGenerator } from "@mikro-orm/migrations"
|
||||
import { isString } from "../../common"
|
||||
import { FilterDef } from "@mikro-orm/core/typings"
|
||||
|
||||
// Monkey patch due to the compilation version issue which prevents us from creating a proper class that extends the TSMigrationGenerator
|
||||
const originalCreateStatement = TSMigrationGenerator.prototype.createStatement
|
||||
|
||||
/**
|
||||
* Safe migration generation for MikroORM
|
||||
*
|
||||
* @param sql The sql statement
|
||||
* @param padLeft The padding
|
||||
*
|
||||
* @example see test file
|
||||
*/
|
||||
TSMigrationGenerator.prototype.createStatement = function (
|
||||
sql: string,
|
||||
padLeft: number
|
||||
) {
|
||||
if (isString(sql)) {
|
||||
if (!sql.includes("create table if not exists")) {
|
||||
sql = sql.replace("create table", "create table if not exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("alter table if exists")) {
|
||||
sql = sql.replace("alter table", "alter table if exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("create index if not exists")) {
|
||||
sql = sql.replace("create index", "create index if not exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("drop index if exists")) {
|
||||
sql = sql.replace("drop index", "drop index if exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("create unique index if not exists")) {
|
||||
sql = sql.replace(
|
||||
"create unique index",
|
||||
"create unique index if not exists"
|
||||
)
|
||||
}
|
||||
|
||||
if (!sql.includes("drop unique index if exists")) {
|
||||
sql = sql.replace("drop unique index", "drop unique index if exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("add column if not exists")) {
|
||||
sql = sql.replace("add column", "add column if not exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("alter column if exists exists")) {
|
||||
sql = sql.replace("alter column", "alter column if exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("drop column if exists")) {
|
||||
sql = sql.replace("drop column", "drop column if exists")
|
||||
}
|
||||
|
||||
if (!sql.includes("drop constraint if exists")) {
|
||||
sql = sql.replace("drop constraint", "drop constraint if exists")
|
||||
}
|
||||
}
|
||||
|
||||
return originalCreateStatement(sql, padLeft)
|
||||
}
|
||||
|
||||
export { TSMigrationGenerator }
|
||||
|
||||
export type Filter = {
|
||||
name?: string
|
||||
} & Omit<FilterDef, "name">
|
||||
|
||||
export async function mikroOrmCreateConnection(
|
||||
database: ModuleServiceInitializeOptions["database"] & {
|
||||
connection?: any
|
||||
filters?: Record<string, Filter>
|
||||
},
|
||||
entities: any[],
|
||||
pathToMigrations: string
|
||||
) {
|
||||
let schema = database.schema || "public"
|
||||
|
||||
let driverOptions = database.driverOptions ?? {
|
||||
connection: { ssl: false },
|
||||
}
|
||||
|
||||
let clientUrl = database.clientUrl
|
||||
|
||||
if (database.connection) {
|
||||
// Reuse already existing connection
|
||||
// It is important that the knex package version is the same as the one used by MikroORM knex package
|
||||
driverOptions = database.connection
|
||||
clientUrl =
|
||||
database.connection.context?.client?.config?.connection?.connectionString
|
||||
schema = database.connection.context?.client?.config?.searchPath
|
||||
}
|
||||
|
||||
const { MikroORM } = await import("@mikro-orm/postgresql")
|
||||
return await MikroORM.init({
|
||||
discovery: { disableDynamicFileAccess: true },
|
||||
entities,
|
||||
debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false,
|
||||
baseDir: process.cwd(),
|
||||
clientUrl,
|
||||
schema,
|
||||
driverOptions,
|
||||
tsNode: process.env.APP_ENV === "development",
|
||||
type: "postgresql",
|
||||
filters: database.filters ?? {},
|
||||
migrations: {
|
||||
disableForeignKeys: false,
|
||||
path: pathToMigrations,
|
||||
generator: TSMigrationGenerator,
|
||||
silent: !(
|
||||
database.debug ??
|
||||
process.env.NODE_ENV?.startsWith("dev") ??
|
||||
false
|
||||
),
|
||||
},
|
||||
schemaGenerator: {
|
||||
disableForeignKeys: false
|
||||
},
|
||||
pool: database.pool as any,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { EntityClass, EntityProperty } from "@mikro-orm/core/typings"
|
||||
import { EntityMetadata, EntitySchema, ReferenceType } from "@mikro-orm/core"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import type { FindOneOptions, FindOptions } from "@mikro-orm/core/drivers"
|
||||
|
||||
export const FreeTextSearchFilterKey = "freeTextSearch"
|
||||
|
||||
interface FilterArgument {
|
||||
value: string
|
||||
fromEntity: string
|
||||
}
|
||||
|
||||
function getEntityProperties(entity: EntityClass<any> | EntitySchema): {
|
||||
[key: string]: EntityProperty<any>
|
||||
} {
|
||||
return (
|
||||
(entity as EntityClass<any>)?.prototype.__meta?.properties ??
|
||||
(entity as EntitySchema).meta?.properties
|
||||
)
|
||||
}
|
||||
|
||||
function retrieveRelationsConstraints(
|
||||
relation: {
|
||||
targetMeta?: EntityMetadata
|
||||
searchable?: boolean
|
||||
mapToPk?: boolean
|
||||
type: string
|
||||
name: string
|
||||
},
|
||||
models: (EntityClass<any> | EntitySchema)[],
|
||||
searchValue: string,
|
||||
visited: Set<string> = new Set(),
|
||||
shouldStop: boolean = false
|
||||
) {
|
||||
if (shouldStop || !relation.searchable) {
|
||||
return
|
||||
}
|
||||
|
||||
const relationClassName = relation.targetMeta!.className
|
||||
|
||||
visited.add(relationClassName)
|
||||
|
||||
const relationFreeTextSearchWhere: any = []
|
||||
|
||||
const relationClass = models.find((m) => m.name === relation.type)!
|
||||
const relationProperties = getEntityProperties(relationClass)
|
||||
|
||||
for (const propertyConfiguration of Object.values(relationProperties)) {
|
||||
if (
|
||||
!(propertyConfiguration as any).searchable ||
|
||||
propertyConfiguration.reference !== ReferenceType.SCALAR
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
relationFreeTextSearchWhere.push({
|
||||
[propertyConfiguration.name]: {
|
||||
$ilike: `%${searchValue}%`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const innerRelations: EntityProperty[] =
|
||||
(relationClass as EntityClass<any>)?.prototype.__meta?.relations ??
|
||||
(relationClass as EntitySchema).meta?.relations
|
||||
|
||||
for (const innerRelation of innerRelations) {
|
||||
const branchVisited = new Set(Array.from(visited))
|
||||
const innerRelationClassName = innerRelation.targetMeta!.className
|
||||
const isSelfCircularDependency =
|
||||
innerRelationClassName === relationClassName
|
||||
|
||||
if (
|
||||
!isSelfCircularDependency &&
|
||||
branchVisited.has(innerRelationClassName)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
branchVisited.add(innerRelationClassName)
|
||||
|
||||
const innerRelationName = !innerRelation.mapToPk
|
||||
? innerRelation.name
|
||||
: relation.targetMeta!.relations.find(
|
||||
(r) => r.type === innerRelation.type && !r.mapToPk
|
||||
)?.name
|
||||
|
||||
if (!innerRelationName) {
|
||||
throw new Error(
|
||||
`Unable to retrieve the counter part relation definition for the mapToPk relation ${innerRelation.name} on entity ${relation.name}`
|
||||
)
|
||||
}
|
||||
|
||||
const relationConstraints = retrieveRelationsConstraints(
|
||||
{
|
||||
name: innerRelationName,
|
||||
targetMeta: innerRelation.targetMeta,
|
||||
searchable: (innerRelation as any).searchable,
|
||||
mapToPk: innerRelation.mapToPk,
|
||||
type: innerRelation.type,
|
||||
},
|
||||
models,
|
||||
searchValue,
|
||||
branchVisited,
|
||||
isSelfCircularDependency
|
||||
)
|
||||
|
||||
if (!relationConstraints?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
relationFreeTextSearchWhere.push({
|
||||
[innerRelationName]: {
|
||||
$or: relationConstraints,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return relationFreeTextSearchWhere
|
||||
}
|
||||
|
||||
export const mikroOrmFreeTextSearchFilterOptionsFactory = (
|
||||
models: (EntityClass<any> | EntitySchema)[]
|
||||
) => {
|
||||
return {
|
||||
cond: (
|
||||
freeTextSearchArgs: FilterArgument,
|
||||
operation: string,
|
||||
manager: SqlEntityManager,
|
||||
options?: (FindOptions<any, any> | FindOneOptions<any, any>) & {
|
||||
visited?: Set<EntityClass<any>>
|
||||
}
|
||||
) => {
|
||||
if (!freeTextSearchArgs || !freeTextSearchArgs.value) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { value, fromEntity } = freeTextSearchArgs
|
||||
|
||||
if (options?.visited?.size) {
|
||||
/**
|
||||
* When being in select in strategy, the filter gets applied to all queries even the ones that are not related to the entity
|
||||
*/
|
||||
const hasFilterAlreadyBeenAppliedForEntity = [
|
||||
...options.visited.values(),
|
||||
].some((v) => v.constructor.name === freeTextSearchArgs.fromEntity)
|
||||
if (hasFilterAlreadyBeenAppliedForEntity) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const entityMetadata = manager.getDriver().getMetadata().get(fromEntity)
|
||||
|
||||
const freeTextSearchWhere = retrieveRelationsConstraints(
|
||||
{
|
||||
targetMeta: entityMetadata,
|
||||
mapToPk: false,
|
||||
searchable: true,
|
||||
type: fromEntity,
|
||||
name: entityMetadata.name!,
|
||||
},
|
||||
models,
|
||||
value
|
||||
)
|
||||
|
||||
if (!freeTextSearchWhere.length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
$or: freeTextSearchWhere,
|
||||
}
|
||||
},
|
||||
default: true,
|
||||
args: false,
|
||||
entity: models.map((m) => m.name) as string[],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
import {
|
||||
BaseFilterable,
|
||||
Context,
|
||||
DAL,
|
||||
FilterQuery,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
RepositoryService,
|
||||
RepositoryTransformOptions,
|
||||
UpsertWithReplaceConfig,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
EntityManager,
|
||||
EntitySchema,
|
||||
LoadStrategy,
|
||||
ReferenceType,
|
||||
RequiredEntityData,
|
||||
wrap,
|
||||
} from "@mikro-orm/core"
|
||||
import { FindOptions as MikroOptions } from "@mikro-orm/core/drivers/IDatabaseDriver"
|
||||
import {
|
||||
EntityClass,
|
||||
EntityName,
|
||||
EntityProperty,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
} from "@mikro-orm/core/typings"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
MedusaError,
|
||||
arrayDifference,
|
||||
isString,
|
||||
promiseAll,
|
||||
} from "../../common"
|
||||
import { buildQuery } from "../../modules-sdk"
|
||||
import {
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||
transactionWrapper,
|
||||
} from "../utils"
|
||||
import { mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
||||
import { mikroOrmSerializer } from "./mikro-orm-serializer"
|
||||
import { dbErrorMapper } from "./db-error-mapper"
|
||||
|
||||
export class MikroOrmBase<T = any> {
|
||||
readonly manager_: any
|
||||
|
||||
protected constructor({ manager }) {
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
getFreshManager<TManager = unknown>(): TManager {
|
||||
return (this.manager_.fork
|
||||
? this.manager_.fork()
|
||||
: this.manager_) as unknown as TManager
|
||||
}
|
||||
|
||||
getActiveManager<TManager = unknown>({
|
||||
transactionManager,
|
||||
manager,
|
||||
}: Context = {}): TManager {
|
||||
return (transactionManager ?? manager ?? this.manager_) as TManager
|
||||
}
|
||||
|
||||
async transaction<TManager = unknown>(
|
||||
task: (transactionManager: TManager) => Promise<any>,
|
||||
options: {
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
transaction?: TManager
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
const freshManager = this.getFreshManager
|
||||
? this.getFreshManager()
|
||||
: this.manager_
|
||||
|
||||
return await transactionWrapper(freshManager, task, options).catch(
|
||||
dbErrorMapper
|
||||
)
|
||||
}
|
||||
|
||||
async serialize<TOutput extends object | object[]>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput> {
|
||||
return await mikroOrmSerializer<TOutput>(data, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Privileged 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 MikroOrmBaseRepository<T extends object = object>
|
||||
extends MikroOrmBase<T>
|
||||
implements RepositoryService<T>
|
||||
{
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
update(data: { entity; update }[], context?: Context): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(
|
||||
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
|
||||
context?: Context
|
||||
): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
upsert(data: unknown[], context: Context = {}): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
upsertWithReplace(
|
||||
data: unknown[],
|
||||
config: UpsertWithReplaceConfig<T> = {
|
||||
relations: [],
|
||||
},
|
||||
context: Context = {}
|
||||
): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
async softDelete(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
sharedContext: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
const isArray = Array.isArray(idsOrFilter)
|
||||
// TODO handle composite keys
|
||||
const filter =
|
||||
isArray || isString(idsOrFilter)
|
||||
? {
|
||||
id: {
|
||||
$in: isArray ? idsOrFilter : [idsOrFilter],
|
||||
},
|
||||
}
|
||||
: idsOrFilter
|
||||
|
||||
const entities = await this.find({ where: filter as any }, sharedContext)
|
||||
const date = new Date()
|
||||
|
||||
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||
await mikroOrmUpdateDeletedAtRecursively<T>(
|
||||
manager,
|
||||
entities as any[],
|
||||
date
|
||||
)
|
||||
|
||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
})
|
||||
|
||||
return [entities, softDeletedEntitiesMap]
|
||||
}
|
||||
|
||||
async restore(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
sharedContext: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
// TODO handle composite keys
|
||||
const isArray = Array.isArray(idsOrFilter)
|
||||
const filter =
|
||||
isArray || isString(idsOrFilter)
|
||||
? {
|
||||
id: {
|
||||
$in: isArray ? idsOrFilter : [idsOrFilter],
|
||||
},
|
||||
}
|
||||
: idsOrFilter
|
||||
|
||||
const query = buildQuery(filter, {
|
||||
withDeleted: true,
|
||||
})
|
||||
|
||||
const entities = await this.find(query, sharedContext)
|
||||
|
||||
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null)
|
||||
|
||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
restored: true,
|
||||
})
|
||||
|
||||
return [entities, softDeletedEntitiesMap]
|
||||
}
|
||||
|
||||
applyFreeTextSearchFilters<T>(
|
||||
findOptions: DAL.FindOptions<T & { q?: string }>,
|
||||
retrieveConstraintsToApply: (q: string) => any[]
|
||||
): void {
|
||||
if (!("q" in findOptions.where) || !findOptions.where.q) {
|
||||
delete findOptions.where.q
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const q = findOptions.where.q as string
|
||||
delete findOptions.where.q
|
||||
|
||||
findOptions.where = {
|
||||
$and: [findOptions.where, { $or: retrieveConstraintsToApply(q) }],
|
||||
} as unknown as DAL.FilterQuery<T & { q?: string }>
|
||||
}
|
||||
}
|
||||
|
||||
export class MikroOrmBaseTreeRepository<
|
||||
T extends object = object
|
||||
> extends MikroOrmBase<T> {
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
find(
|
||||
options?: DAL.FindOptions,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
create(data: unknown, context?: Context): Promise<T> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(id: string, context?: Context): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
entity: any
|
||||
): {
|
||||
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
|
||||
} {
|
||||
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
|
||||
// @ts-ignore
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
|
||||
return new Proxy(this, {
|
||||
get: (target, prop) => {
|
||||
if (typeof target[prop] === "function") {
|
||||
return (...args) => {
|
||||
const res = target[prop].bind(target)(...args)
|
||||
if (res instanceof Promise) {
|
||||
return res.catch(dbErrorMapper)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
return target[prop]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static buildUniqueCompositeKeyValue(keys: string[], data: object) {
|
||||
return keys.map((k) => data[k]).join("_")
|
||||
}
|
||||
|
||||
static retrievePrimaryKeys(entity: EntityClass<T> | EntitySchema<T>) {
|
||||
return (
|
||||
(entity as EntitySchema<T>).meta?.primaryKeys ??
|
||||
(entity as EntityClass<T>).prototype.__meta.primaryKeys ?? ["id"]
|
||||
)
|
||||
}
|
||||
|
||||
async create(data: any[], context?: Context): Promise<T[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const entities = data.map((data_) => {
|
||||
return manager.create(
|
||||
entity as EntityName<T>,
|
||||
data_ as RequiredEntityData<T>
|
||||
)
|
||||
})
|
||||
|
||||
manager.persist(entities)
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
async update(data: { entity; update }[], context?: Context): Promise<T[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
const entities = data.map((data_) => {
|
||||
return manager.assign(data_.entity, data_.update)
|
||||
})
|
||||
|
||||
manager.persist(entities)
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
async delete(
|
||||
filters: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
|
||||
context?: Context
|
||||
): Promise<void> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
await manager.nativeDelete<T>(entity as EntityName<T>, filters as any)
|
||||
}
|
||||
|
||||
async find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...options }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
if (!("strategy" in findOptions_.options)) {
|
||||
if (findOptions_.options.limit != null || findOptions_.options.offset) {
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
} else {
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.JOINED,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return await manager.find(
|
||||
entity as EntityName<T>,
|
||||
findOptions_.where as MikroFilterQuery<T>,
|
||||
findOptions_.options as MikroOptions<T>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[T[], number]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
entity as EntityName<T>,
|
||||
findOptions_.where as MikroFilterQuery<T>,
|
||||
findOptions_.options as MikroOptions<T>
|
||||
)
|
||||
}
|
||||
|
||||
async upsert(data: any[], context: Context = {}): Promise<T[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const primaryKeys =
|
||||
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
|
||||
|
||||
let primaryKeysCriteria: { [key: string]: any }[] = []
|
||||
if (primaryKeys.length === 1) {
|
||||
const primaryKeyValues = data
|
||||
.map((d) => d[primaryKeys[0]])
|
||||
.filter(Boolean)
|
||||
|
||||
if (primaryKeyValues.length) {
|
||||
primaryKeysCriteria.push({
|
||||
[primaryKeys[0]]: primaryKeyValues,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
primaryKeysCriteria = data.map((d) => ({
|
||||
$and: primaryKeys.map((key) => ({ [key]: d[key] })),
|
||||
}))
|
||||
}
|
||||
|
||||
let allEntities: T[][] = []
|
||||
|
||||
if (primaryKeysCriteria.length) {
|
||||
allEntities = await Promise.all(
|
||||
primaryKeysCriteria.map(
|
||||
async (criteria) =>
|
||||
await this.find(
|
||||
{ where: criteria } as DAL.FindOptions<T>,
|
||||
context
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const existingEntities = allEntities.flat()
|
||||
|
||||
const existingEntitiesMap = new Map<string, T>()
|
||||
existingEntities.forEach((entity) => {
|
||||
if (entity) {
|
||||
const key =
|
||||
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
|
||||
primaryKeys,
|
||||
entity
|
||||
)
|
||||
existingEntitiesMap.set(key, entity)
|
||||
}
|
||||
})
|
||||
|
||||
const upsertedEntities: T[] = []
|
||||
const createdEntities: T[] = []
|
||||
const updatedEntities: T[] = []
|
||||
|
||||
data.forEach((data_) => {
|
||||
// In case the data provided are just strings, then we build an object with the primary key as the key and the data as the valuecd -
|
||||
const key =
|
||||
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
|
||||
primaryKeys,
|
||||
data_
|
||||
)
|
||||
|
||||
const existingEntity = existingEntitiesMap.get(key)
|
||||
if (existingEntity) {
|
||||
const updatedType = manager.assign(existingEntity, data_)
|
||||
updatedEntities.push(updatedType)
|
||||
} else {
|
||||
const newEntity = manager.create<T>(entity, data_)
|
||||
createdEntities.push(newEntity)
|
||||
}
|
||||
})
|
||||
|
||||
if (createdEntities.length) {
|
||||
manager.persist(createdEntities)
|
||||
upsertedEntities.push(...createdEntities)
|
||||
}
|
||||
|
||||
if (updatedEntities.length) {
|
||||
manager.persist(updatedEntities)
|
||||
upsertedEntities.push(...updatedEntities)
|
||||
}
|
||||
|
||||
// TODO return the all, created, updated entities
|
||||
return upsertedEntities
|
||||
}
|
||||
|
||||
// UpsertWithReplace does several things to simplify module implementation.
|
||||
// For each entry of your base entity, it will go through all one-to-many and many-to-many relations, and it will do a diff between what is passed and what is in the database.
|
||||
// For each relation, it create new entries (without an ID), it will associate existing entries (with only an ID), and it will update existing entries (with an ID and other fields).
|
||||
// Finally, it will delete the relation entries that were omitted in the new data.
|
||||
// The response is a POJO of the data that was written to the DB, including all new IDs. The order is preserved with the input.
|
||||
// Limitations: We expect that IDs are used as primary keys, and we don't support composite keys.
|
||||
// We only support 1-level depth of upserts. We don't support custom fields on the many-to-many pivot tables for now
|
||||
async upsertWithReplace(
|
||||
data: any[],
|
||||
config: UpsertWithReplaceConfig<T> = {
|
||||
relations: [],
|
||||
},
|
||||
context: Context = {}
|
||||
): Promise<T[]> {
|
||||
if (!data.length) {
|
||||
return []
|
||||
}
|
||||
// We want to convert a potential ORM model to a POJO
|
||||
const normalizedData: any[] = await this.serialize(data)
|
||||
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
// Handle the relations
|
||||
const allRelations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.name).relations
|
||||
|
||||
const nonexistentRelations = arrayDifference(
|
||||
(config.relations as any) ?? [],
|
||||
allRelations.map((r) => r.name)
|
||||
)
|
||||
|
||||
if (nonexistentRelations.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Nonexistent relations were passed during upsert: ${nonexistentRelations}`
|
||||
)
|
||||
}
|
||||
|
||||
// We want to response with all the data including the IDs in the same order as the input. We also include data that was passed but not processed.
|
||||
const reconstructedResponse: any[] = []
|
||||
const originalDataMap = new Map<string, T>()
|
||||
|
||||
// Create only the top-level entity without the relations first
|
||||
const toUpsert = normalizedData.map((entry) => {
|
||||
// Make a copy of the data and remove undefined fields. The data is already a POJO due to the serialization above
|
||||
const entryCopy = JSON.parse(JSON.stringify(entry))
|
||||
const reconstructedEntry: any = {}
|
||||
|
||||
allRelations?.forEach((relation) => {
|
||||
reconstructedEntry[relation.name] = this.handleRelationAssignment_(
|
||||
relation,
|
||||
entryCopy
|
||||
)
|
||||
})
|
||||
|
||||
const mainEntity = this.getEntityWithId(manager, entity.name, entryCopy)
|
||||
reconstructedResponse.push({ ...mainEntity, ...reconstructedEntry })
|
||||
originalDataMap.set(mainEntity.id, entry)
|
||||
|
||||
return mainEntity
|
||||
})
|
||||
|
||||
const upsertedTopLevelEntities = await this.upsertMany_(
|
||||
manager,
|
||||
entity.name,
|
||||
toUpsert
|
||||
)
|
||||
|
||||
await promiseAll(
|
||||
upsertedTopLevelEntities
|
||||
.map((entityEntry, i) => {
|
||||
const originalEntry = originalDataMap.get((entityEntry as any).id)!
|
||||
const reconstructedEntry = reconstructedResponse[i]
|
||||
|
||||
return allRelations?.map(async (relation) => {
|
||||
const relationName = relation.name as keyof T
|
||||
if (!config.relations?.includes(relationName)) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Handle ONE_TO_ONE
|
||||
// One to one and Many to one are handled outside of the assignment as they need to happen before the main entity is created
|
||||
if (
|
||||
relation.reference === ReferenceType.ONE_TO_ONE ||
|
||||
relation.reference === ReferenceType.MANY_TO_ONE
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
reconstructedEntry[relationName] =
|
||||
await this.assignCollectionRelation_(
|
||||
manager,
|
||||
{ ...originalEntry, id: (entityEntry as any).id },
|
||||
relation
|
||||
)
|
||||
return
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
)
|
||||
|
||||
// // We want to populate the identity map with the data that was written to the DB, and return an entity object
|
||||
// return reconstructedResponse.map((r) =>
|
||||
// manager.create(entity, r, { persist: false })
|
||||
// )
|
||||
|
||||
return reconstructedResponse
|
||||
}
|
||||
|
||||
// FUTURE: We can make this performant by only aggregating the operations, but only executing them at the end.
|
||||
protected async assignCollectionRelation_(
|
||||
manager: SqlEntityManager,
|
||||
data: T,
|
||||
relation: EntityProperty
|
||||
) {
|
||||
const dataForRelation = data[relation.name]
|
||||
// If the field is not set, we ignore it. Null and empty arrays are a valid input and are handled below
|
||||
if (dataForRelation === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Make sure the data is correctly initialized with IDs before using it
|
||||
const normalizedData = dataForRelation.map((normalizedItem) => {
|
||||
return this.getEntityWithId(manager, relation.type, normalizedItem)
|
||||
})
|
||||
|
||||
if (relation.reference === ReferenceType.MANY_TO_MANY) {
|
||||
const currentPivotColumn = relation.inverseJoinColumns[0]
|
||||
const parentPivotColumn = relation.joinColumns[0]
|
||||
|
||||
if (!normalizedData.length) {
|
||||
await manager.nativeDelete(relation.pivotEntity, {
|
||||
[parentPivotColumn]: (data as any).id,
|
||||
})
|
||||
|
||||
return normalizedData
|
||||
}
|
||||
|
||||
await this.upsertMany_(manager, relation.type, normalizedData, true)
|
||||
|
||||
const pivotData = normalizedData.map((currModel) => {
|
||||
return {
|
||||
[parentPivotColumn]: (data as any).id,
|
||||
[currentPivotColumn]: currModel.id,
|
||||
}
|
||||
})
|
||||
|
||||
const qb = manager.qb(relation.pivotEntity)
|
||||
await qb.insert(pivotData).onConflict().ignore().execute()
|
||||
|
||||
await manager.nativeDelete(relation.pivotEntity, {
|
||||
[parentPivotColumn]: (data as any).id,
|
||||
[currentPivotColumn]: {
|
||||
$nin: pivotData.map((d) => d[currentPivotColumn]),
|
||||
},
|
||||
})
|
||||
|
||||
return normalizedData
|
||||
}
|
||||
|
||||
if (relation.reference === ReferenceType.ONE_TO_MANY) {
|
||||
const joinColumns =
|
||||
relation.targetMeta?.properties[relation.mappedBy]?.joinColumns
|
||||
|
||||
const joinColumnsConstraints = {}
|
||||
joinColumns?.forEach((joinColumn, index) => {
|
||||
const referencedColumnName = relation.referencedColumnNames[index]
|
||||
joinColumnsConstraints[joinColumn] = data[referencedColumnName]
|
||||
})
|
||||
|
||||
if (normalizedData.length) {
|
||||
normalizedData.forEach((normalizedDataItem: any) => {
|
||||
Object.assign(normalizedDataItem, {
|
||||
...joinColumnsConstraints,
|
||||
})
|
||||
})
|
||||
|
||||
await this.upsertMany_(manager, relation.type, normalizedData)
|
||||
}
|
||||
|
||||
await manager.nativeDelete(relation.type, {
|
||||
...joinColumnsConstraints,
|
||||
id: { $nin: normalizedData.map((d: any) => d.id) },
|
||||
})
|
||||
|
||||
return normalizedData
|
||||
}
|
||||
|
||||
return normalizedData
|
||||
}
|
||||
|
||||
protected handleRelationAssignment_(
|
||||
relation: EntityProperty<any>,
|
||||
entryCopy: T
|
||||
) {
|
||||
const originalData = entryCopy[relation.name]
|
||||
delete entryCopy[relation.name]
|
||||
|
||||
if (originalData === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// If it is a many-to-one we ensure the ID is set for when we want to set/unset an association
|
||||
if (relation.reference === ReferenceType.MANY_TO_ONE) {
|
||||
if (originalData === null) {
|
||||
entryCopy[relation.joinColumns[0]] = null
|
||||
return null
|
||||
}
|
||||
|
||||
// The relation can either be a primitive or the entity object, depending on how it's defined on the model
|
||||
let relationId
|
||||
if (isString(originalData)) {
|
||||
relationId = originalData
|
||||
} else if ("id" in originalData) {
|
||||
relationId = originalData.id
|
||||
}
|
||||
|
||||
// We don't support creating many-to-one relations, so we want to throw if someone doesn't pass the ID
|
||||
if (!relationId) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Many-to-one relation ${relation.name} must be set with an ID`
|
||||
)
|
||||
}
|
||||
|
||||
entryCopy[relation.joinColumns[0]] = relationId
|
||||
return originalData
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Returns a POJO object with the ID populated from the entity model hooks
|
||||
protected getEntityWithId(
|
||||
manager: SqlEntityManager,
|
||||
entityName: string,
|
||||
data: any
|
||||
): Record<string, any> & { id: string } {
|
||||
const created = manager.create(entityName, data, {
|
||||
managed: false,
|
||||
persist: false,
|
||||
})
|
||||
|
||||
const resp = {
|
||||
// `create` will omit non-existent fields, but we want to pass the data the user provided through so the correct errors get thrown
|
||||
...data,
|
||||
...(created as any).__helper.__bignumberdata,
|
||||
id: (created as any).id,
|
||||
}
|
||||
|
||||
// Non-persist relation columns should be removed before we do the upsert.
|
||||
Object.entries((created as any).__helper?.__meta.properties ?? {})
|
||||
.filter(
|
||||
([_, propDef]: any) =>
|
||||
propDef.persist === false &&
|
||||
propDef.reference === ReferenceType.MANY_TO_ONE
|
||||
)
|
||||
.forEach(([key]) => {
|
||||
delete resp[key]
|
||||
})
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
protected async upsertMany_(
|
||||
manager: SqlEntityManager,
|
||||
entityName: string,
|
||||
entries: any[],
|
||||
skipUpdate: boolean = false
|
||||
) {
|
||||
const selectQb = manager.qb(entityName)
|
||||
const existingEntities: any[] = await selectQb.select("*").where({
|
||||
id: { $in: entries.map((d) => d.id) },
|
||||
})
|
||||
|
||||
const existingEntitiesMap = new Map(
|
||||
existingEntities.map((e) => [e.id, e])
|
||||
)
|
||||
|
||||
const orderedEntities: T[] = []
|
||||
|
||||
await promiseAll(
|
||||
entries.map(async (data) => {
|
||||
const existingEntity = existingEntitiesMap.get(data.id)
|
||||
orderedEntities.push(data)
|
||||
if (existingEntity) {
|
||||
if (skipUpdate) {
|
||||
return
|
||||
}
|
||||
await manager.nativeUpdate(entityName, { id: data.id }, data)
|
||||
} else {
|
||||
const qb = manager.qb(entityName)
|
||||
if (skipUpdate) {
|
||||
await qb.insert(data).onConflict().ignore().execute()
|
||||
} else {
|
||||
await manager.insert(entityName, data)
|
||||
// await manager.insert(entityName, data)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return orderedEntities
|
||||
}
|
||||
}
|
||||
|
||||
return MikroOrmAbstractBaseRepository_ as unknown as {
|
||||
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import {
|
||||
Collection,
|
||||
EntityDTO,
|
||||
EntityMetadata,
|
||||
helper,
|
||||
IPrimaryKey,
|
||||
Loaded,
|
||||
Platform,
|
||||
Reference,
|
||||
ReferenceType,
|
||||
SerializationContext,
|
||||
Utils,
|
||||
} from "@mikro-orm/core"
|
||||
import { SerializeOptions } from "@mikro-orm/core/serialization/EntitySerializer"
|
||||
|
||||
function isVisible<T extends object>(
|
||||
meta: EntityMetadata<T>,
|
||||
propName: string,
|
||||
options: SerializeOptions<T, any> & { preventCircularRef?: boolean } = {}
|
||||
): boolean {
|
||||
if (options.populate === true) {
|
||||
return options.populate
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(options.populate) &&
|
||||
options.populate?.find(
|
||||
(item) => item === propName || item.startsWith(propName + ".")
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (options.exclude?.find((item) => item === propName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const prop = meta.properties[propName]
|
||||
const visible = prop && !prop.hidden
|
||||
const prefixed = prop && !prop.primary && propName.startsWith("_") // ignore prefixed properties, if it's not a PK
|
||||
|
||||
return visible && !prefixed
|
||||
}
|
||||
|
||||
function isPopulated<T extends object>(
|
||||
entity: T,
|
||||
propName: string,
|
||||
options: SerializeOptions<T, any>
|
||||
): boolean {
|
||||
if (
|
||||
typeof options.populate !== "boolean" &&
|
||||
options.populate?.find(
|
||||
(item) => item === propName || item.startsWith(propName + ".")
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof options.populate === "boolean") {
|
||||
return options.populate
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer property filtering for the serialization which takes into account the parent entity to filter out circular references if configured for.
|
||||
* @param propName
|
||||
* @param meta
|
||||
* @param options
|
||||
* @param parent
|
||||
*/
|
||||
function filterEntityPropToSerialize(
|
||||
propName: string,
|
||||
meta: EntityMetadata,
|
||||
options: SerializeOptions<object, any> & {
|
||||
preventCircularRef?: boolean
|
||||
} = {},
|
||||
parent?: object
|
||||
): boolean {
|
||||
const isVisibleRes = isVisible(meta, propName, options)
|
||||
const prop = meta.properties[propName]
|
||||
|
||||
// Only prevent circular references if prop is a relation
|
||||
if (
|
||||
prop &&
|
||||
options.preventCircularRef &&
|
||||
isVisibleRes &&
|
||||
parent &&
|
||||
prop.reference !== ReferenceType.SCALAR
|
||||
) {
|
||||
// mapToPk would represent a foreign key and we want to keep them
|
||||
return !!prop.mapToPk || parent.constructor.name !== prop.type
|
||||
}
|
||||
return isVisibleRes
|
||||
}
|
||||
|
||||
export class EntitySerializer {
|
||||
static serialize<T extends object, P extends string = never>(
|
||||
entity: T,
|
||||
options: SerializeOptions<T, P> & { preventCircularRef?: boolean } = {},
|
||||
parent?: object
|
||||
): EntityDTO<Loaded<T, P>> {
|
||||
const wrapped = helper(entity)
|
||||
const meta = wrapped.__meta
|
||||
let contextCreated = false
|
||||
|
||||
if (!wrapped.__serializationContext.root) {
|
||||
const root = new SerializationContext<T>()
|
||||
SerializationContext.propagate(
|
||||
root,
|
||||
entity,
|
||||
(meta, prop) =>
|
||||
meta.properties[prop]?.reference !== ReferenceType.SCALAR
|
||||
)
|
||||
contextCreated = true
|
||||
}
|
||||
|
||||
const root = wrapped.__serializationContext.root!
|
||||
const ret = {} as EntityDTO<Loaded<T, P>>
|
||||
const keys = new Set<string>(meta.primaryKeys)
|
||||
Object.keys(entity).forEach((prop) => keys.add(prop))
|
||||
const visited = root.visited.has(entity)
|
||||
|
||||
if (!visited) {
|
||||
root.visited.add(entity)
|
||||
}
|
||||
|
||||
;[...keys]
|
||||
/** Medusa Custom properties filtering **/
|
||||
.filter((prop) =>
|
||||
filterEntityPropToSerialize(prop, meta, options, parent)
|
||||
)
|
||||
.map((prop) => {
|
||||
const cycle = root.visit(meta.className, prop)
|
||||
|
||||
if (cycle && visited) {
|
||||
return [prop, undefined]
|
||||
}
|
||||
|
||||
const val = this.processProperty<T>(
|
||||
prop as keyof T & string,
|
||||
entity,
|
||||
options
|
||||
)
|
||||
|
||||
if (!cycle) {
|
||||
root.leave(meta.className, prop)
|
||||
}
|
||||
|
||||
if (options.skipNull && Utils.isPlainObject(val)) {
|
||||
Utils.dropUndefinedProperties(val, null)
|
||||
}
|
||||
|
||||
return [prop, val]
|
||||
})
|
||||
.filter(
|
||||
([, value]) =>
|
||||
typeof value !== "undefined" && !(value === null && options.skipNull)
|
||||
)
|
||||
.forEach(
|
||||
([prop, value]) =>
|
||||
(ret[
|
||||
this.propertyName(
|
||||
meta,
|
||||
prop as keyof T & string,
|
||||
wrapped.__platform
|
||||
)
|
||||
] = value as T[keyof T & string])
|
||||
)
|
||||
|
||||
if (contextCreated) {
|
||||
root.close()
|
||||
}
|
||||
|
||||
if (!wrapped.isInitialized()) {
|
||||
return ret
|
||||
}
|
||||
|
||||
// decorated getters
|
||||
meta.props
|
||||
.filter(
|
||||
(prop) =>
|
||||
prop.getter &&
|
||||
prop.getterName === undefined &&
|
||||
typeof entity[prop.name] !== "undefined" &&
|
||||
isVisible(meta, prop.name, options)
|
||||
)
|
||||
.forEach(
|
||||
(prop) =>
|
||||
(ret[this.propertyName(meta, prop.name, wrapped.__platform)] =
|
||||
this.processProperty(prop.name, entity, options))
|
||||
)
|
||||
|
||||
// decorated get methods
|
||||
meta.props
|
||||
.filter(
|
||||
(prop) =>
|
||||
prop.getterName &&
|
||||
(entity[prop.getterName] as unknown) instanceof Function &&
|
||||
isVisible(meta, prop.name, options)
|
||||
)
|
||||
.forEach(
|
||||
(prop) =>
|
||||
(ret[this.propertyName(meta, prop.name, wrapped.__platform)] =
|
||||
this.processProperty(
|
||||
prop.getterName as keyof T & string,
|
||||
entity,
|
||||
options
|
||||
))
|
||||
)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private static propertyName<T>(
|
||||
meta: EntityMetadata<T>,
|
||||
prop: keyof T & string,
|
||||
platform?: Platform
|
||||
): string {
|
||||
/* istanbul ignore next */
|
||||
if (meta.properties[prop]?.serializedName) {
|
||||
return meta.properties[prop].serializedName as keyof T & string
|
||||
}
|
||||
|
||||
if (meta.properties[prop]?.primary && platform) {
|
||||
return platform.getSerializedPrimaryKeyField(prop) as keyof T & string
|
||||
}
|
||||
|
||||
return prop
|
||||
}
|
||||
|
||||
private static processProperty<T extends object>(
|
||||
prop: keyof T & string,
|
||||
entity: T,
|
||||
options: SerializeOptions<T, any>
|
||||
): T[keyof T] | undefined {
|
||||
const parts = prop.split(".")
|
||||
prop = parts[0] as string & keyof T
|
||||
const wrapped = helper(entity)
|
||||
const property = wrapped.__meta.properties[prop]
|
||||
const serializer = property?.serializer
|
||||
|
||||
// getter method
|
||||
if ((entity[prop] as unknown) instanceof Function) {
|
||||
const returnValue = (
|
||||
entity[prop] as unknown as () => T[keyof T & string]
|
||||
)()
|
||||
if (!options.ignoreSerializers && serializer) {
|
||||
return serializer(returnValue)
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (!options.ignoreSerializers && serializer) {
|
||||
return serializer(entity[prop])
|
||||
}
|
||||
|
||||
if (Utils.isCollection(entity[prop])) {
|
||||
return this.processCollection(prop, entity, options)
|
||||
}
|
||||
|
||||
if (Utils.isEntity(entity[prop], true)) {
|
||||
return this.processEntity(prop, entity, wrapped.__platform, options)
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (property?.reference === ReferenceType.EMBEDDED) {
|
||||
if (Array.isArray(entity[prop])) {
|
||||
return (entity[prop] as object[]).map((item) =>
|
||||
helper(item).toJSON()
|
||||
) as T[keyof T]
|
||||
}
|
||||
|
||||
if (Utils.isObject(entity[prop])) {
|
||||
return helper(entity[prop]).toJSON() as T[keyof T]
|
||||
}
|
||||
}
|
||||
|
||||
const customType = property?.customType
|
||||
|
||||
if (customType) {
|
||||
return customType.toJSON(entity[prop], wrapped.__platform)
|
||||
}
|
||||
|
||||
return wrapped.__platform.normalizePrimaryKey(
|
||||
entity[prop] as unknown as IPrimaryKey
|
||||
) as unknown as T[keyof T]
|
||||
}
|
||||
|
||||
private static extractChildOptions<T extends object, U extends object>(
|
||||
options: SerializeOptions<T, any>,
|
||||
prop: keyof T & string
|
||||
): SerializeOptions<U, any> {
|
||||
const extractChildElements = (items: string[]) => {
|
||||
return items
|
||||
.filter((field) => field.startsWith(`${prop}.`))
|
||||
.map((field) => field.substring(prop.length + 1))
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
populate: Array.isArray(options.populate)
|
||||
? extractChildElements(options.populate)
|
||||
: options.populate,
|
||||
exclude: Array.isArray(options.exclude)
|
||||
? extractChildElements(options.exclude)
|
||||
: options.exclude,
|
||||
} as SerializeOptions<U, any>
|
||||
}
|
||||
|
||||
private static processEntity<T extends object>(
|
||||
prop: keyof T & string,
|
||||
entity: T,
|
||||
platform: Platform,
|
||||
options: SerializeOptions<T, any>
|
||||
): T[keyof T] | undefined {
|
||||
const child = Reference.unwrapReference(entity[prop] as T)
|
||||
const wrapped = helper(child)
|
||||
const populated =
|
||||
isPopulated(child, prop, options) && wrapped.isInitialized()
|
||||
const expand = populated || options.forceObject || !wrapped.__managed
|
||||
|
||||
if (expand) {
|
||||
return this.serialize(
|
||||
child,
|
||||
this.extractChildOptions(options, prop),
|
||||
/** passing the entity as the parent for circular filtering **/
|
||||
entity
|
||||
) as T[keyof T]
|
||||
}
|
||||
|
||||
return platform.normalizePrimaryKey(
|
||||
wrapped.getPrimaryKey() as IPrimaryKey
|
||||
) as T[keyof T]
|
||||
}
|
||||
|
||||
private static processCollection<T extends object>(
|
||||
prop: keyof T & string,
|
||||
entity: T,
|
||||
options: SerializeOptions<T, any>
|
||||
): T[keyof T] | undefined {
|
||||
const col = entity[prop] as unknown as Collection<T>
|
||||
|
||||
if (!col.isInitialized()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return col.getItems(false).map((item) => {
|
||||
if (isPopulated(item, prop, options)) {
|
||||
return this.serialize(
|
||||
item,
|
||||
this.extractChildOptions(options, prop),
|
||||
/** passing the entity as the parent for circular filtering **/
|
||||
entity
|
||||
)
|
||||
}
|
||||
|
||||
return helper(item).getPrimaryKey()
|
||||
}) as unknown as T[keyof T]
|
||||
}
|
||||
}
|
||||
|
||||
export const mikroOrmSerializer = <TOutput extends object>(
|
||||
data: any,
|
||||
options?: Parameters<typeof EntitySerializer.serialize>[1] & {
|
||||
preventCircularRef?: boolean
|
||||
}
|
||||
): TOutput => {
|
||||
options ??= {}
|
||||
|
||||
const data_ = (Array.isArray(data) ? data : [data]).filter(Boolean)
|
||||
|
||||
const forSerialization: unknown[] = []
|
||||
const notForSerialization: unknown[] = []
|
||||
|
||||
data_.forEach((object) => {
|
||||
if (object.__meta) {
|
||||
return forSerialization.push(object)
|
||||
}
|
||||
|
||||
return notForSerialization.push(object)
|
||||
})
|
||||
|
||||
let result: any = forSerialization.map((entity) =>
|
||||
EntitySerializer.serialize(entity, {
|
||||
forceObject: true,
|
||||
populate: true,
|
||||
preventCircularRef: true,
|
||||
...options,
|
||||
} as SerializeOptions<any, any>)
|
||||
) as TOutput[]
|
||||
|
||||
if (notForSerialization.length) {
|
||||
result = result.concat(notForSerialization)
|
||||
}
|
||||
|
||||
return Array.isArray(data) ? result : result[0]
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { buildQuery } from "../../modules-sdk"
|
||||
import { EntityMetadata, FindOptions, wrap } from "@mikro-orm/core"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
|
||||
function detectCircularDependency(
|
||||
manager: SqlEntityManager,
|
||||
entityMetadata: EntityMetadata,
|
||||
visited: Set<string> = new Set(),
|
||||
shouldStop: boolean = false
|
||||
) {
|
||||
if (shouldStop) {
|
||||
return
|
||||
}
|
||||
|
||||
visited.add(entityMetadata.className)
|
||||
|
||||
const relations = entityMetadata.relations
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
const branchVisited = new Set(Array.from(visited))
|
||||
|
||||
const isSelfCircularDependency = entityMetadata.class === relation.entity()
|
||||
|
||||
if (!isSelfCircularDependency && branchVisited.has(relation.name)) {
|
||||
const dependencies = Array.from(visited)
|
||||
dependencies.push(entityMetadata.className)
|
||||
const circularDependencyStr = dependencies.join(" -> ")
|
||||
|
||||
throw new Error(
|
||||
`Unable to soft delete the ${relation.name}. Circular dependency detected: ${circularDependencyStr}`
|
||||
)
|
||||
}
|
||||
branchVisited.add(relation.name)
|
||||
|
||||
const relationEntityMetadata = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(relation.type)
|
||||
|
||||
detectCircularDependency(
|
||||
manager,
|
||||
relationEntityMetadata,
|
||||
branchVisited,
|
||||
isSelfCircularDependency
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function performCascadingSoftDeletion<T>(
|
||||
manager: SqlEntityManager,
|
||||
entity: T & { id: string; deleted_at?: string | Date | null },
|
||||
value: Date | null
|
||||
) {
|
||||
if (!("deleted_at" in entity)) return
|
||||
|
||||
entity.deleted_at = value
|
||||
|
||||
const entityName = entity.constructor.name
|
||||
|
||||
const relations = manager.getDriver().getMetadata().get(entityName).relations
|
||||
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
let entityRelation = entity[relation.name]
|
||||
|
||||
// Handle optional relationships
|
||||
if (relation.nullable && !entityRelation) {
|
||||
continue
|
||||
}
|
||||
|
||||
const retrieveEntity = async () => {
|
||||
const query = buildQuery(
|
||||
{
|
||||
id: entity.id,
|
||||
},
|
||||
{
|
||||
relations: [relation.name],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
return await manager.findOne(
|
||||
entity.constructor.name,
|
||||
query.where,
|
||||
query.options as FindOptions<any>
|
||||
)
|
||||
}
|
||||
|
||||
if (!entityRelation) {
|
||||
// Fixes the case of many to many through pivot table
|
||||
entityRelation = await retrieveEntity()
|
||||
if (!entityRelation) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const isCollection = "toArray" in entityRelation
|
||||
let relationEntities: any[] = []
|
||||
|
||||
if (isCollection) {
|
||||
if (!entityRelation.isInitialized()) {
|
||||
entityRelation = await retrieveEntity()
|
||||
entityRelation = entityRelation[relation.name]
|
||||
}
|
||||
relationEntities = entityRelation.getItems()
|
||||
} else {
|
||||
const wrappedEntity = wrap(entityRelation)
|
||||
const initializedEntityRelation = wrappedEntity.isInitialized()
|
||||
? entityRelation
|
||||
: await wrap(entityRelation).init()
|
||||
relationEntities = [initializedEntityRelation]
|
||||
}
|
||||
|
||||
if (!relationEntities.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
||||
}
|
||||
|
||||
await manager.persist(entity)
|
||||
}
|
||||
|
||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
||||
T extends object = any
|
||||
>(
|
||||
manager: SqlEntityManager,
|
||||
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
||||
value: Date | null
|
||||
) => {
|
||||
for (const entity of entities) {
|
||||
const entityMetadata = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.constructor.name)
|
||||
detectCircularDependency(manager, entityMetadata)
|
||||
await performCascadingSoftDeletion(manager, entity, value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./load-custom-repositories"
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Constructor, DAL } from "@medusajs/types"
|
||||
import { asClass } from "awilix"
|
||||
import { lowerCaseFirst } from "../../common"
|
||||
|
||||
/**
|
||||
* Load the repositories from the custom repositories object. If a repository is not
|
||||
* present in the custom repositories object, the default repository will be used.
|
||||
*
|
||||
* @param customRepositories
|
||||
* @param container
|
||||
*/
|
||||
export function loadCustomRepositories({
|
||||
defaultRepositories,
|
||||
customRepositories,
|
||||
container,
|
||||
}) {
|
||||
const customRepositoriesMap = new Map(Object.entries(customRepositories))
|
||||
|
||||
Object.entries(defaultRepositories).forEach(([key, defaultRepository]) => {
|
||||
let finalRepository = customRepositoriesMap.get(key)
|
||||
|
||||
if (
|
||||
!finalRepository ||
|
||||
!(finalRepository as Constructor<DAL.RepositoryService>).prototype.find
|
||||
) {
|
||||
finalRepository = defaultRepository
|
||||
}
|
||||
|
||||
container.register({
|
||||
[lowerCaseFirst(key)]: asClass(
|
||||
finalRepository as Constructor<DAL.RepositoryService>
|
||||
).singleton(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { isObject } from "../common"
|
||||
|
||||
export async function transactionWrapper<TManager = unknown>(
|
||||
manager: any,
|
||||
task: (transactionManager: any) => 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 = manager.transaction ?? manager.transactional
|
||||
return await transactionMethod.bind(manager)(task, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to create a new Object that collect the entities
|
||||
* based on the columnLookup. This is useful when you want to soft delete entities and return
|
||||
* an object where the keys are the entities name and the values are the entities
|
||||
* that were soft deleted.
|
||||
*
|
||||
* @param entities
|
||||
* @param deletedEntitiesMap
|
||||
* @param getEntityName
|
||||
*/
|
||||
export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
deletedEntitiesMap,
|
||||
getEntityName,
|
||||
restored,
|
||||
}: {
|
||||
entities: any[]
|
||||
deletedEntitiesMap?: Map<string, any[]>
|
||||
getEntityName?: (entity: any) => string
|
||||
restored?: boolean
|
||||
}): Record<string, any[]> {
|
||||
deletedEntitiesMap ??= new Map<string, any[]>()
|
||||
getEntityName ??= (entity) => entity.constructor.name
|
||||
|
||||
for (const entity of entities) {
|
||||
const entityName = getEntityName(entity)
|
||||
const shouldSkip = !!deletedEntitiesMap
|
||||
.get(entityName)
|
||||
?.some((e) => e.id === entity.id)
|
||||
|
||||
if ((restored ? !!entity.deleted_at : !entity.deleted_at) || shouldSkip) {
|
||||
continue
|
||||
}
|
||||
|
||||
const values = deletedEntitiesMap.get(entityName) ?? []
|
||||
values.push(entity)
|
||||
deletedEntitiesMap.set(entityName, values)
|
||||
|
||||
Object.values(entity).forEach((propValue: any) => {
|
||||
if (propValue != null && isObject(propValue[0])) {
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities: propValue,
|
||||
deletedEntitiesMap,
|
||||
getEntityName,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Object.fromEntries(deletedEntitiesMap)
|
||||
}
|
||||
Reference in New Issue
Block a user