Skip to content

Commit

Permalink
feat: Normalize known DB errors to a MedusaError when possible
Browse files Browse the repository at this point in the history
  • Loading branch information
sradevski committed Apr 3, 2024
1 parent 56c04f4 commit 82d369b
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 125 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-jeans-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/utils": minor
---

Return normalized DB errors from the mikro ORM repository
9 changes: 3 additions & 6 deletions integration-tests/api/__tests__/admin/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -2630,8 +2630,7 @@ medusaIntegrationTestRunner({
)
})

// TODO: This needs to be fixed
it.skip("successfully creates product with soft-deleted product handle and deletes it again", async () => {
it("successfully creates product with soft-deleted product handle and deletes it again", async () => {
// First we soft-delete the product
const response = await api
.delete(`/admin/products/${baseProduct.id}`, adminHeaders)
Expand Down Expand Up @@ -2708,8 +2707,7 @@ medusaIntegrationTestRunner({
expect(response.data.id).toEqual(baseCollection.id)
})

// TODO: This needs to be fixed, it returns 422 now.
it.skip("successfully creates soft-deleted product collection", async () => {
it("successfully creates soft-deleted product collection", async () => {
const response = await api.delete(
`/admin/collections/${baseCollection.id}`,
adminHeaders
Expand Down Expand Up @@ -2749,8 +2747,7 @@ medusaIntegrationTestRunner({
}
})

// TODO: This needs to be fixed
it.skip("successfully creates soft-deleted product variant", async () => {
it("successfully creates soft-deleted product variant", async () => {
const variant = baseProduct.variants[0]
const response = await api.delete(
`/admin/products/${baseProduct.id}/variants/${variant.id}`,
Expand Down
8 changes: 4 additions & 4 deletions packages/tax/integration-tests/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ moduleIntegrationTestRunner({
],
})
).rejects.toThrowError(
/You are trying to create a Tax Rate Rule for a reference that already exists. Tax Rate id: .*?, reference id: product_id_1./
/Tax rate rule with tax_rate_id: .*?, reference_id: product_id_1 already exists./
)

const rate = await service.create({
Expand All @@ -729,7 +729,7 @@ moduleIntegrationTestRunner({
reference_id: "product_id_1",
})
).rejects.toThrowError(
/You are trying to create a Tax Rate Rule for a reference that already exists. Tax Rate id: .*?, reference id: product_id_1./
/Tax rate rule with tax_rate_id: .*?, reference_id: product_id_1 already exists./
)
})

Expand Down Expand Up @@ -764,7 +764,7 @@ moduleIntegrationTestRunner({
province_code: "QC",
})
).rejects.toThrowError(
"You are trying to create a Tax Region for (country_code: ca, province_code: qc) but one already exists."
"Tax region with country_code: ca, province_code: qc already exists."
)
})

Expand Down Expand Up @@ -797,7 +797,7 @@ moduleIntegrationTestRunner({
is_default: true,
})
).rejects.toThrowError(
/You are trying to create a default tax rate for region: .*? which already has a default tax rate. Unset the current default rate and try again./
/Tax rate with tax_region_id: .*? already exists./
)
})

Expand Down
74 changes: 4 additions & 70 deletions packages/tax/src/services/tax-module-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ import {
} from "@medusajs/utils"
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { singleDefaultRegionIndexName } from "../models/tax-rate"
import { uniqueRateReferenceIndexName } from "../models/tax-rate-rule"
import { countryCodeProvinceIndexName } from "../models/tax-region"

type InjectedDependencies = {
baseRepository: DAL.RepositoryService
Expand Down Expand Up @@ -106,11 +103,7 @@ export default class TaxModuleService<
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> {
const input = Array.isArray(data) ? data : [data]
const rates = await this.create_(input, sharedContext).catch((err) => {
this.handleCreateError(err)
this.handleCreateRulesError(err)
throw err
})
const rates = await this.create_(input, sharedContext)
return Array.isArray(data) ? rates : rates[0]
}

Expand Down Expand Up @@ -182,13 +175,7 @@ export default class TaxModuleService<
data: TaxTypes.UpdateTaxRateDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
const rates = await this.update_(selector, data, sharedContext).catch(
(err) => {
this.handleCreateError(err)
this.handleCreateRulesError(err)
throw err
}
)
const rates = await this.update_(selector, data, sharedContext)
const serialized = await this.baseRepository_.serialize<
TaxTypes.TaxRateDTO[]
>(rates, { populate: true })
Expand Down Expand Up @@ -322,12 +309,7 @@ export default class TaxModuleService<
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const result = await this.createTaxRegions_(input, sharedContext).catch(
(err) => {
this.handleCreateRegionsError(err)
throw err
}
)
const result = await this.createTaxRegions_(input, sharedContext)
return Array.isArray(data) ? result : result[0]
}

Expand Down Expand Up @@ -382,12 +364,7 @@ export default class TaxModuleService<
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const result = await this.createTaxRateRules_(input, sharedContext).catch(
(err) => {
this.handleCreateRulesError(err)
throw err
}
)
const result = await this.createTaxRateRules_(input, sharedContext)
return Array.isArray(data) ? result : result[0]
}

Expand Down Expand Up @@ -748,49 +725,6 @@ export default class TaxModuleService<
return code.toLowerCase()
}

private handleCreateRegionsError(err: any) {
if (err.constraint === countryCodeProvinceIndexName) {
const [countryCode, provinceCode] = err.detail
.split("=")[1]
.match(/\(([^)]+)\)/)[1]
.split(",")

throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You are trying to create a Tax Region for (country_code: ${countryCode.trim()}, province_code: ${provinceCode.trim()}) but one already exists.`
)
}
}

private handleCreateError(err: any) {
if (err.constraint === singleDefaultRegionIndexName) {
// err.detail = Key (tax_region_id)=(txreg_01HQX5E8GEH36ZHJWFYDAFY67P) already exists.
const regionId = err.detail.split("=")[1].match(/\(([^)]+)\)/)[1]

throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You are trying to create a default tax rate for region: ${regionId} which already has a default tax rate. Unset the current default rate and try again.`
)
}
}

private handleCreateRulesError(err: any) {
if (err.constraint === uniqueRateReferenceIndexName) {
// err.detail == "Key (tax_rate_id, reference_id)=(txr_01HQWRXTC0JK0F02D977WRR45T, product_id_1) already exists."
// We want to extract the ids from the detail string
// i.e. txr_01HQWRXTC0JK0F02D977WRR45T and product_id_1
const [taxRateId, referenceId] = err.detail
.split("=")[1]
.match(/\(([^)]+)\)/)[1]
.split(",")

throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You are trying to create a Tax Rate Rule for a reference that already exists. Tax Rate id: ${taxRateId.trim()}, reference id: ${referenceId.trim()}.`
)
}
}

// @InjectTransactionManager("baseRepository_")
// async createProvidersOnLoad(@MedusaContext() sharedContext: Context = {}) {
// const providersToLoad = this.container_["tax_providers"] as ITaxProvider[]
Expand Down
82 changes: 82 additions & 0 deletions packages/utils/src/dal/mikro-orm/db-error-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
ForeignKeyConstraintViolationException,
InvalidFieldNameException,
NotFoundError,
NotNullConstraintViolationException,
UniqueConstraintViolationException,
} from "@mikro-orm/core"
import { MedusaError, upperCaseFirst } from "../../common"

export const dbErrorMapper = (err: Error) => {
if (err instanceof NotFoundError) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, err.message)
}

if (err instanceof UniqueConstraintViolationException) {
const info = getConstraintInfo(err)
if (!info) {
throw err
}

throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`${upperCaseFirst(info.table)} with ${info.keys
.map((key, i) => `${key}: ${info.values[i]}`)
.join(", ")} already exists.`
)
}

if (err instanceof NotNullConstraintViolationException) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot set field '${(err as any).column}' of ${upperCaseFirst(
(err as any).table
)} to null`
)
}

if (err instanceof InvalidFieldNameException) {
const userFriendlyMessage = err.message.match(/(column.*)/)?.[0]
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
userFriendlyMessage ?? err.message
)
}

if (err instanceof ForeignKeyConstraintViolationException) {
const info = getConstraintInfo(err)
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`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()),
}
}
Loading

0 comments on commit 82d369b

Please sign in to comment.