Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Normalize known DB errors to a MedusaError when possible #6922

Merged
merged 1 commit into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
25 changes: 19 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 All @@ -2771,6 +2768,22 @@ medusaIntegrationTestRunner({
ean: variant.ean,
upc: variant.upc,
barcode: variant.barcode,
...breaking(
() => ({
options: [
{
option_id: baseProduct.options[0].id,
value: "newval",
},
{
option_id: baseProduct.options[1].id,
value: "newval",
},
],
}),
// TODO: Require that options are passed if they belong on the product, and the combos are unique per variant
() => ({})
),
prices: [
{
currency_code: "usd",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ medusaIntegrationTestRunner({
})
})

it("should throw error when shipping option does not exist", async () => {
it.only("should throw error when shipping option does not exist", async () => {
const { response } = await api
.post(
`/admin/fulfillment/shipping-options/does-not-exist/rules/batch/add`,
Expand All @@ -100,7 +100,7 @@ medusaIntegrationTestRunner({
expect(response.data).toEqual({
type: "not_found",
message:
"Shipping_option with shipping_option_id does-not-exist does not exist.",
"You tried to set relationship shipping_option_id: does-not-exist, but such entity does not exist",
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ moduleIntegrationTestRunner({
],
}
await expect(service.create(customerData)).rejects.toThrow(
"A default shipping address already exists"
/Customer address with customer_id: .*? already exists./
)
})

Expand Down Expand Up @@ -727,7 +727,9 @@ moduleIntegrationTestRunner({
country_code: "US",
is_default_shipping: true,
})
).rejects.toThrow("A default shipping address already exists")
).rejects.toThrow(
/Customer address with customer_id: .*? already exists./
)
})

it("should only be possible to add one default billing address per customer", async () => {
Expand Down Expand Up @@ -761,7 +763,9 @@ moduleIntegrationTestRunner({
country_code: "US",
is_default_billing: true,
})
).rejects.toThrow("A default billing address already exists")
).rejects.toThrow(
/Customer address with customer_id: .*? already exists./
)
})
})

Expand Down Expand Up @@ -899,7 +903,9 @@ moduleIntegrationTestRunner({

await expect(
service.updateAddresses(address.id, { is_default_shipping: true })
).rejects.toThrow("A default shipping address already exists")
).rejects.toThrow(
/Customer address with customer_id: .*? already exists./
)
})
})

Expand Down
31 changes: 3 additions & 28 deletions packages/customer/src/services/customer-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ export default class CustomerModuleService<
| CustomerTypes.CreateCustomerDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[]> {
const customers = await this.create_(dataOrArray, sharedContext).catch(
this.handleDbErrors
)
const customers = await this.create_(dataOrArray, sharedContext)

const serialized = await this.baseRepository_.serialize<
CustomerTypes.CustomerDTO[]
Expand Down Expand Up @@ -351,9 +349,7 @@ export default class CustomerModuleService<
): Promise<
CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[]
> {
const addresses = await this.addAddresses_(data, sharedContext).catch(
this.handleDbErrors
)
const addresses = await this.addAddresses_(data, sharedContext)

const serialized = await this.baseRepository_.serialize<
CustomerTypes.CustomerAddressDTO[]
Expand Down Expand Up @@ -434,7 +430,7 @@ export default class CustomerModuleService<
sharedContext
)

await this.flush(sharedContext).catch(this.handleDbErrors)
await this.flush(sharedContext)

const serialized = await this.baseRepository_.serialize<
CustomerTypes.CustomerAddressDTO[]
Expand Down Expand Up @@ -475,25 +471,4 @@ export default class CustomerModuleService<
const em = (context.manager ?? context.transactionManager) as EntityManager
await em.flush()
}

private async handleDbErrors(err: any) {
if (isDuplicateError(err)) {
switch (err.constraint) {
case UNIQUE_CUSTOMER_SHIPPING_ADDRESS:
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
"A default shipping address already exists"
)
case UNIQUE_CUSTOMER_BILLING_ADDRESS:
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
"A default billing address already exists"
)
default:
break
}
}

throw err
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ moduleIntegrationTestRunner({
const err = await service.create(data).catch((e) => e)

expect(err).toBeDefined()
expect(err.constraint).toBe("IDX_fulfillment_set_name_unique")
expect(err.message).toContain("exists")
})

it("should fail on creating a new fulfillment set with new service zones and new geo zones that are not valid", async function () {
Expand Down Expand Up @@ -837,7 +837,7 @@ moduleIntegrationTestRunner({
const err = await service.update(updateData).catch((e) => e)

expect(err).toBeDefined()
expect(err.constraint).toBe("IDX_fulfillment_set_name_unique")
expect(err.message).toContain("exists")
})

it("should update a collection of fulfillment sets and replace old service zones by new ones", async function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ moduleIntegrationTestRunner({
const err = await service.createServiceZones(data).catch((e) => e)

expect(err).toBeDefined()
expect(err.constraint).toBe("IDX_service_zone_name_unique")
expect(err.message).toContain("exists")
})

it("should fail on creating a service zone and new geo zones that are not valid", async function () {
Expand Down Expand Up @@ -435,7 +435,7 @@ moduleIntegrationTestRunner({
.catch((e) => e)

expect(err).toBeDefined()
expect(err.constraint).toBe("IDX_service_zone_name_unique")
expect(err.message).toContain("exists")
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ moduleIntegrationTestRunner({
.catch((e) => e)

expect(err).toBeDefined()
expect(err.constraint).toBe("IDX_shipping_profile_name_unique")
expect(err.message).toContain("exists")
})
})
})
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
Loading
Loading