chore: Migrate region module to use DML (#7837)

This commit is contained in:
Oli Juhl authored and GitHub committed 2024-06-29 13:14:52 +00:00
1 parent 7c3f5cc334
commit fa6cd84049
10 files changed
+173 -155

No files matched your search

@@ -1,6 +1,6 @@
import { buildQuery } from "../../modules-sdk"
import { EntityMetadata, FindOptions, wrap } from "@mikro-orm/core" import { EntityMetadata, FindOptions, wrap } from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { buildQuery } from "../../modules-sdk"
function detectCircularDependency( function detectCircularDependency(
manager: SqlEntityManager, manager: SqlEntityManager,
@@ -15,8 +15,9 @@ function detectCircularDependency(
visited.add(entityMetadata.className) visited.add(entityMetadata.className)
const relations = entityMetadata.relations const relations = entityMetadata.relations
const relationsToCascade = relations.filter((relation) => const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any) relation.cascade?.includes("soft-remove" as any)
) )
for (const relation of relationsToCascade) { for (const relation of relationsToCascade) {
@@ -63,7 +64,7 @@ async function performCascadingSoftDeletion<T>(
const relations = manager.getDriver().getMetadata().get(entityName).relations const relations = manager.getDriver().getMetadata().get(entityName).relations
const relationsToCascade = relations.filter((relation) => const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any) relation.cascade?.includes("soft-remove" as any)
) )
for (const relation of relationsToCascade) { for (const relation of relationsToCascade) {
@@ -9,9 +9,9 @@ import {
BeforeCreate, BeforeCreate,
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OnInit,
OneToMany, OneToMany,
OneToOne, OneToOne,
OnInit,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common" import { camelToSnakeCase, pluralize } from "../../../common"
@@ -1,12 +1,7 @@
import { defineMikroOrmCliConfig } from "@medusajs/utils"
import * as entities from "./src/models" import * as entities from "./src/models"
import { TSMigrationGenerator } from "@medusajs/utils"
module.exports = { export default defineMikroOrmCliConfig({
entities: Object.values(entities), entities: Object.values(entities),
schema: "public", databaseName: "medusa-region",
clientUrl: "postgres://postgres@localhost/medusa-region", })
type: "postgresql",
migrations: {
generator: TSMigrationGenerator,
},
}
@@ -1,13 +1,14 @@
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { ContainerRegistrationKeys, DefaultsUtils } from "@medusajs/utils" import { ContainerRegistrationKeys, DefaultsUtils } from "@medusajs/utils"
import { Country } from "@models" import { RegionCountry } from "@models"
export default async ({ container }: LoaderOptions): Promise<void> => { export default async ({ container }: LoaderOptions): Promise<void> => {
// TODO: Add default logger to the container when running tests // TODO: Add default logger to the container when running tests
const logger = const logger =
container.resolve<Logger>(ContainerRegistrationKeys.LOGGER) ?? console container.resolve<Logger>(ContainerRegistrationKeys.LOGGER) ?? console
const countryService_: ModulesSdkTypes.IMedusaInternalService<Country> = const countryService_: ModulesSdkTypes.IMedusaInternalService<
container.resolve("countryService") typeof RegionCountry
> = container.resolve("countryService")
try { try {
const normalizedCountries = DefaultsUtils.defaultCountries.map((c) => ({ const normalizedCountries = DefaultsUtils.defaultCountries.map((c) => ({
@@ -1,5 +1,7 @@
{ {
"namespaces": ["public"], "namespaces": [
"public"
],
"name": "public", "name": "public",
"tables": [ "tables": [
{ {
@@ -86,16 +88,11 @@
"name": "region", "name": "region",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{
"columnNames": ["deleted_at"],
"composite": false,
"keyName": "IDX_region_deleted_at",
"primary": false,
"unique": false
},
{ {
"keyName": "region_pkey", "keyName": "region_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -159,14 +156,65 @@
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"mappedType": "text" "mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
} }
}, },
"name": "region_country", "name": "region_country",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{
"keyName": "IDX_region_country_region_id_iso_2_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_region_country_region_id_iso_2_unique\" ON \"region_country\" (region_id, iso_2)"
},
{ {
"keyName": "region_country_pkey", "keyName": "region_country_pkey",
"columnNames": ["iso_2"], "columnNames": [
"iso_2"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -176,9 +224,13 @@
"foreignKeys": { "foreignKeys": {
"region_country_region_id_foreign": { "region_country_region_id_foreign": {
"constraintName": "region_country_region_id_foreign", "constraintName": "region_country_region_id_foreign",
"columnNames": ["region_id"], "columnNames": [
"region_id"
],
"localTableName": "public.region_country", "localTableName": "public.region_country",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.region", "referencedTableName": "public.region",
"deleteRule": "set null", "deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
@@ -0,0 +1,39 @@
import { generatePostgresAlterColummnIfExistStatement } from "@medusajs/utils"
import { Migration } from "@mikro-orm/migrations"
export class Migration20240624200006 extends Migration {
async up(): Promise<void> {
this.addSql(
'ALTER TABLE IF EXISTS "region_country" ADD COLUMN IF NOT EXISTS "metadata" jsonb null, ADD COLUMN "created_at" timestamptz NOT NULL DEFAULT NOW(), ADD COLUMN "updated_at" timestamptz NOT NULL DEFAULT NOW(), ADD COLUMN "deleted_at" timestamptz NULL;'
)
this.addSql(
generatePostgresAlterColummnIfExistStatement(
"region_country",
["region_id"],
`DROP NOT NULL`
)
)
}
async down(): Promise<void> {
this.addSql(
'ALTER TABLE IF EXISTS "region_country" DROP COLUMN IF EXISTS "metadata";'
)
this.addSql(
'ALTER TABLE IF EXISTS "region_country" DROP COLUMN IF EXISTS "created_at";'
)
this.addSql(
'ALTER TABLE IF EXISTS "region_country" DROP COLUMN IF EXISTS "updated_at";'
)
this.addSql(
'ALTER TABLE IF EXISTS "region_country" DROP COLUMN IF EXISTS "deleted_at";'
)
this.addSql(
generatePostgresAlterColummnIfExistStatement(
"region_country",
["region_id"],
`SET NOT NULL`
)
)
}
}
+25 -41
View File
@@ -1,44 +1,28 @@
import { Entity, ManyToOne, PrimaryKey, Property } from "@mikro-orm/core" import { model } from "@medusajs/utils"
import { createPsqlIndexStatementHelper } from "@medusajs/utils"
import Region from "./region" import Region from "./region"
// We don't need a partial index on deleted_at here since we don't support soft deletes on countries const Country = model
const regionIdIsoIndexName = "IDX_region_country_region_id_iso_2_unique" .define(
const RegionIdIsoIndexStatement = createPsqlIndexStatementHelper({ { name: "Country", tableName: "region_country" },
name: regionIdIsoIndexName, {
tableName: "region_country", iso_2: model.text().searchable().primaryKey(),
columns: ["region_id", "iso_2"], iso_3: model.text(),
unique: true, num_code: model.text(),
}) name: model.text().searchable(),
display_name: model.text(),
region: model
.belongsTo(() => Region, { mappedBy: "countries" })
.nullable(),
metadata: model.json().nullable(),
}
)
.indexes([
{
// TODO: Remove ts-ignore when field inference takes into account the nullable property
// @ts-ignore
on: ["region_id", "iso_2"],
unique: true,
},
])
RegionIdIsoIndexStatement.MikroORMIndex() export default Country
@Entity({ tableName: "region_country" })
export default class Country {
@PrimaryKey({ columnType: "text" })
@Property({ columnType: "text" })
iso_2: string
@Property({ columnType: "text" })
iso_3: string
@Property({ columnType: "text" })
num_code: string
@Property({ columnType: "text" })
name: string
@Property({ columnType: "text" })
display_name: string
@Property({ columnType: "text", nullable: true })
region_id?: string | null = null
@ManyToOne({
entity: () => Region,
fieldName: "region_id",
nullable: true,
onDelete: "set null",
})
region?: Region | null
}
+2 -1
View File
@@ -1,2 +1,3 @@
export { default as Country } from "./country" export { default as RegionCountry } from "./country"
export { default as Region } from "./region" export { default as Region } from "./region"
+11 -70
View File
@@ -1,72 +1,13 @@
import { DAL } from "@medusajs/types" import { model } from "@medusajs/utils"
import { DALUtils, Searchable, generateEntityId } from "@medusajs/utils" import RegionCountry from "./country"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
OnInit,
OneToMany,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Country from "./country"
type RegionOptionalProps = "countries" | DAL.SoftDeletableEntityDateColumns const Region = model.define("region", {
id: model.id({ prefix: "reg" }),
name: model.text().searchable(),
currency_code: model.text().searchable(),
automatic_taxes: model.boolean().default(true),
countries: model.hasMany(() => RegionCountry),
metadata: model.json().nullable(),
})
@Entity({ tableName: "region" }) export default Region
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class Region {
[OptionalProps]?: RegionOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Searchable()
@Property({ columnType: "text" })
name: string
@Searchable()
@Property({ columnType: "text" })
currency_code: string
@Property({ columnType: "boolean" })
automatic_taxes: boolean = true
@OneToMany(() => Country, (country) => country.region)
countries = new Collection<Country>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_region_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "reg")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "reg")
}
}
@@ -3,8 +3,9 @@ import {
CreateRegionDTO, CreateRegionDTO,
DAL, DAL,
FilterableRegionProps, FilterableRegionProps,
InternalModuleDeclaration,
IRegionModuleService, IRegionModuleService,
InferEntityType,
InternalModuleDeclaration,
ModuleJoinerConfig, ModuleJoinerConfig,
ModulesSdkTypes, ModulesSdkTypes,
RegionCountryDTO, RegionCountryDTO,
@@ -14,20 +15,18 @@ import {
UpsertRegionDTO, UpsertRegionDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
arrayDifference,
getDuplicates,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isString,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
MedusaService, MedusaService,
arrayDifference,
getDuplicates,
isString,
promiseAll, promiseAll,
removeUndefined, removeUndefined,
} from "@medusajs/utils" } from "@medusajs/utils"
import { RegionCountry as Country, Region } from "@models"
import { Country, Region } from "@models"
import { UpdateRegionInput } from "@types" import { UpdateRegionInput } from "@types"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -49,8 +48,12 @@ export default class RegionModuleService
implements IRegionModuleService implements IRegionModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly regionService_: ModulesSdkTypes.IMedusaInternalService<Region> protected readonly regionService_: ModulesSdkTypes.IMedusaInternalService<
protected readonly countryService_: ModulesSdkTypes.IMedusaInternalService<Country> typeof Region
>
protected readonly countryService_: ModulesSdkTypes.IMedusaInternalService<
typeof Country
>
constructor( constructor(
{ baseRepository, regionService, countryService }: InjectedDependencies, { baseRepository, regionService, countryService }: InjectedDependencies,
@@ -67,7 +70,7 @@ export default class RegionModuleService
return joinerConfig return joinerConfig
} }
//@ts-expect-error // @ts-expect-error
async createRegions( async createRegions(
data: CreateRegionDTO[], data: CreateRegionDTO[],
sharedContext?: Context sharedContext?: Context
@@ -95,7 +98,7 @@ export default class RegionModuleService
async createRegions_( async createRegions_(
data: CreateRegionDTO[], data: CreateRegionDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<Region[]> { ): Promise<InferEntityType<typeof Region>[]> {
let normalizedInput = RegionModuleService.normalizeInput(data) let normalizedInput = RegionModuleService.normalizeInput(data)
let normalizedDbRegions = normalizedInput.map((region) => let normalizedDbRegions = normalizedInput.map((region) =>
@@ -172,7 +175,7 @@ export default class RegionModuleService
(region): region is CreateRegionDTO => !region.id (region): region is CreateRegionDTO => !region.id
) )
const operations: Promise<Region[]>[] = [] const operations: Promise<InferEntityType<typeof Region>[]>[] = []
if (forCreate.length) { if (forCreate.length) {
operations.push(this.createRegions_(forCreate, sharedContext)) operations.push(this.createRegions_(forCreate, sharedContext))
@@ -187,7 +190,7 @@ export default class RegionModuleService
) )
} }
//@ts-expect-error // @ts-expect-error
async updateRegions( async updateRegions(
id: string, id: string,
data: UpdateRegionDTO, data: UpdateRegionDTO,
@@ -237,7 +240,7 @@ export default class RegionModuleService
protected async updateRegions_( protected async updateRegions_(
data: UpdateRegionInput[], data: UpdateRegionInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<Region[]> { ): Promise<InferEntityType<typeof Region>[]> {
const normalizedInput = RegionModuleService.normalizeInput(data) const normalizedInput = RegionModuleService.normalizeInput(data)
// If countries are being updated for a region, first make previously set countries' region to null to get to a clean slate. // If countries are being updated for a region, first make previously set countries' region to null to get to a clean slate.
@@ -306,7 +309,7 @@ export default class RegionModuleService
private async validateCountries( private async validateCountries(
countries: string[] | undefined, countries: string[] | undefined,
sharedContext: Context sharedContext: Context
): Promise<Country[]> { ): Promise<InferEntityType<typeof Country>[]> {
if (!countries?.length) { if (!countries?.length) {
return [] return []
} }
@@ -343,6 +346,7 @@ export default class RegionModuleService
} }
// Countries that already have a region already assigned to them // Countries that already have a region already assigned to them
// @ts-ignore
const countriesWithRegion = countriesInDb.filter((c) => !!c.region_id) const countriesWithRegion = countriesInDb.filter((c) => !!c.region_id)
if (countriesWithRegion.length) { if (countriesWithRegion.length) {
throw new MedusaError( throw new MedusaError(