Merge branch 'develop' into fix-cartServiceCreateLostShippingAddress

This commit is contained in:
Adrien de Peretti authored and GitHub committed 2022-05-10 11:16:19 +02:00
commit 65186036b5
29 files changed
+1176 -818

No files matched your search

@@ -211,6 +211,117 @@ describe("/admin/price-lists", () => {
])
)
})
it("given a search query, returns matching results by name", async () => {
const api = useApi()
const response = await api
.get("/admin/price-lists?q=winter", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.warn(err.response.data)
})
expect(response.status).toEqual(200)
expect(response.data.price_lists).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "VIP winter sale",
}),
])
)
expect(response.data.count).toEqual(1)
})
it("given a search query, returns matching results by description", async () => {
const api = useApi()
const response = await api
.get("/admin/price-lists?q=25%", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.warn(err.response.data)
})
expect(response.status).toEqual(200)
expect(response.data.price_lists).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "VIP winter sale",
description:
"Winter sale for VIP customers. 25% off selected items.",
}),
])
)
expect(response.data.count).toEqual(1)
})
it("given a search query, returns empty list when does not exist", async () => {
const api = useApi()
const response = await api
.get("/admin/price-lists?q=blablabla", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.warn(err.response.data)
})
expect(response.status).toEqual(200)
expect(response.data.price_lists).toEqual([])
expect(response.data.count).toEqual(0)
})
it("given a search query and a status filter not matching any price list, returns an empty set", async () => {
const api = useApi()
const response = await api
.get("/admin/price-lists?q=vip&status[]=draft", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.warn(err.response.data)
})
expect(response.status).toEqual(200)
expect(response.data.price_lists).toEqual([])
expect(response.data.count).toEqual(0)
})
it("given a search query and a status filter matching a price list, returns a price list", async () => {
const api = useApi()
const response = await api
.get("/admin/price-lists?q=vip&status[]=active", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.warn(err.response.data)
})
expect(response.status).toEqual(200)
expect(response.data.price_lists).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "VIP winter sale",
status: "active",
}),
])
)
expect(response.data.count).toEqual(1)
})
})
describe("POST /admin/price-lists/:id", () => {
@@ -794,9 +905,17 @@ describe("/admin/price-lists", () => {
await simplePriceListFactory(dbConnection, {
id: "test-list",
customer_groups: ["test-group"],
prices: [
{ variant_id: "test-variant-1", currency_code: "usd", amount: 100 },
{ variant_id: "test-variant-4", currency_code: "usd", amount: 100 },
{ variant_id: "test-variant-1", currency_code: "usd", amount: 150 },
{ variant_id: "test-variant-4", currency_code: "usd", amount: 150 },
],
})
await simplePriceListFactory(dbConnection, {
id: "test-list-2",
prices: [
{ variant_id: "test-variant-1", currency_code: "usd", amount: 200 },
{ variant_id: "test-variant-4", currency_code: "usd", amount: 200 },
],
})
} catch (err) {
@@ -810,7 +929,7 @@ describe("/admin/price-lists", () => {
await db.teardown()
})
it("lists only product 1, 2", async () => {
it("lists only product 1, 2 with price list prices", async () => {
const api = useApi()
const response = await api
@@ -826,8 +945,50 @@ describe("/admin/price-lists", () => {
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(2)
expect(response.data.products).toEqual([
expect.objectContaining({ id: "test-prod-1" }),
expect.objectContaining({ id: "test-prod-2" }),
expect.objectContaining({
id: "test-prod-1",
variants: [
expect.objectContaining({
id: "test-variant-1",
prices: [
expect.objectContaining({ currency_code: "usd", amount: 100 }),
expect.objectContaining({
currency_code: "usd",
amount: 150,
price_list_id: "test-list",
}),
],
}),
expect.objectContaining({
id: "test-variant-2",
prices: [
expect.objectContaining({ currency_code: "usd", amount: 100 }),
],
}),
],
}),
expect.objectContaining({
id: "test-prod-2",
variants: [
expect.objectContaining({
id: "test-variant-3",
prices: [
expect.objectContaining({ currency_code: "usd", amount: 100 }),
],
}),
expect.objectContaining({
id: "test-variant-4",
prices: [
expect.objectContaining({ currency_code: "usd", amount: 100 }),
expect.objectContaining({
currency_code: "usd",
amount: 150,
price_list_id: "test-list",
}),
],
}),
],
}),
])
})
@@ -6,7 +6,7 @@ const { initDb, useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder")
const productSeeder = require("../../helpers/product-seeder")
const { ProductVariant } = require("@medusajs/medusa")
const { ProductVariant, ProductOptionValue, MoneyAmount } = require("@medusajs/medusa")
const priceListSeeder = require("../../helpers/price-list-seeder")
jest.setTimeout(50000)
@@ -1737,6 +1737,177 @@ describe("/admin/products", () => {
expect(variant).toEqual(undefined)
})
it("successfully deletes a product variant and its associated option values", async () => {
const api = useApi()
// Validate that the option value exists
const optValPre = await dbConnection.manager.findOne(ProductOptionValue, {
variant_id: "test-variant_2",
})
expect(optValPre).not.toEqual(undefined)
// Soft delete the variant
const response = await api.delete(
"/admin/products/test-product/variants/test-variant_2",
{
headers: {
Authorization: "Bearer test_token",
},
}
)
expect(response.status).toEqual(200)
// Validate that the option value was deleted
const optValPost = await dbConnection.manager.findOne(
ProductOptionValue,
{
variant_id: "test-variant_2",
}
)
expect(optValPost).toEqual(undefined)
// Validate that the option still exists in the DB with deleted_at
const optValDeleted = await dbConnection.manager.findOne(ProductOptionValue, {
variant_id: "test-variant_2",
}, {
withDeleted: true,
})
expect(optValDeleted).toEqual(expect.objectContaining({
deleted_at: expect.any(Date),
variant_id: "test-variant_2",
}))
})
it("successfully deletes a product and any option value associated with one of its variants", async () => {
const api = useApi()
// Validate that the option value exists
const optValPre = await dbConnection.manager.findOne(ProductOptionValue, {
variant_id: "test-variant_2",
})
expect(optValPre).not.toEqual(undefined)
// Soft delete the product
const response = await api.delete("/admin/products/test-product", {
headers: {
Authorization: "Bearer test_token",
},
})
expect(response.status).toEqual(200)
// Validate that the option value has been deleted
const optValPost = await dbConnection.manager.findOne(
ProductOptionValue,
{
variant_id: "test-variant_2",
}
)
expect(optValPost).toEqual(undefined)
// Validate that the option still exists in the DB with deleted_at
const optValDeleted = await dbConnection.manager.findOne(ProductOptionValue, {
variant_id: "test-variant_2",
}, {
withDeleted: true,
})
expect(optValDeleted).toEqual(expect.objectContaining({
deleted_at: expect.any(Date),
variant_id: "test-variant_2",
}))
})
it("successfully deletes a product variant and its associated prices", async () => {
const api = useApi()
// Validate that the price exists
const pricePre = await dbConnection.manager.findOne(MoneyAmount, {
id: "test-price",
})
expect(pricePre).not.toEqual(undefined)
// Soft delete the variant
const response = await api.delete(
"/admin/products/test-product/variants/test-variant",
{
headers: {
Authorization: "Bearer test_token",
},
}
)
expect(response.status).toEqual(200)
// Validate that the price was deleted
const pricePost = await dbConnection.manager.findOne(
MoneyAmount,
{
id: "test-price",
}
)
expect(pricePost).toEqual(undefined)
// Validate that the price still exists in the DB with deleted_at
const optValDeleted = await dbConnection.manager.findOne(MoneyAmount, {
id: "test-price",
}, {
withDeleted: true,
})
expect(optValDeleted).toEqual(expect.objectContaining({
deleted_at: expect.any(Date),
id: "test-price",
}))
})
it("successfully deletes a product and any prices associated with one of its variants", async () => {
const api = useApi()
// Validate that the price exists
const pricePre = await dbConnection.manager.findOne(MoneyAmount, {
id: "test-price",
})
expect(pricePre).not.toEqual(undefined)
// Soft delete the product
const response = await api.delete("/admin/products/test-product", {
headers: {
Authorization: "Bearer test_token",
},
})
expect(response.status).toEqual(200)
// Validate that the price has been deleted
const pricePost = await dbConnection.manager.findOne(MoneyAmount, {
id: "test-price",
})
expect(pricePost).toEqual(undefined)
// Validate that the price still exists in the DB with deleted_at
const optValDeleted = await dbConnection.manager.findOne(MoneyAmount, {
id: "test-price",
}, {
withDeleted: true,
})
expect(optValDeleted).toEqual(expect.objectContaining({
deleted_at: expect.any(Date),
id: "test-price",
}))
})
it("successfully creates product with soft-deleted product handle and deletes it again", async () => {
const api = useApi()
@@ -3,6 +3,7 @@ import {
MoneyAmount,
PriceListType,
PriceListStatus,
CustomerGroup,
} from "@medusajs/medusa"
import faker from "faker"
import { Connection } from "typeorm"
@@ -38,6 +39,28 @@ export const simplePriceListFactory = async (
const manager = connection.manager
const listId = data.id || `simple-price-list-${Math.random() * 1000}`
let customerGroups = []
if (typeof data.customer_groups !== "undefined") {
await manager
.createQueryBuilder()
.insert()
.into(CustomerGroup)
.values(
data.customer_groups.map((group) => ({
id: group,
name: faker.company.companyName(),
}))
)
.orIgnore()
.execute()
customerGroups = await manager.findByIds(
CustomerGroup,
data.customer_groups
)
}
const toCreate = {
id: listId,
name: data.name || faker.commerce.productName(),
@@ -46,6 +69,7 @@ export const simplePriceListFactory = async (
type: data.type || PriceListType.OVERRIDE,
starts_at: data.starts_at || null,
ends_at: data.ends_at || null,
customer_groups: customerGroups,
}
const toSave = manager.create(PriceList, toCreate)
+3 -3
View File
@@ -8,16 +8,16 @@
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "1.2.1-dev-1649181615374",
"@medusajs/medusa": "1.2.1-dev-1650573289860",
"faker": "^5.5.3",
"medusa-interfaces": "1.2.1-dev-1649181615374",
"medusa-interfaces": "1.2.1-dev-1650573289860",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"babel-preset-medusa-package": "1.1.19-dev-1649181615374",
"babel-preset-medusa-package": "1.1.19-dev-1650573289860",
"jest": "^26.6.3"
}
}
+34 -37
View File
@@ -1301,10 +1301,10 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@medusajs/medusa-cli@1.2.1-dev-1649181615374":
version "1.2.1-dev-1649181615374"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1649181615374.tgz#1ea9014e3ec9813a52457b0d6e2fc6bb64d3bfd6"
integrity sha512-8m6Z1ZZqstZKaAaKoFS3v3IzI7BFhcBgpF+iCSRuJoXltQgzVQOAxXuPjkRoi+m1ZZ+Yi/YYEzKmNQ99vmXisQ==
"@medusajs/medusa-cli@1.2.1-dev-1650573289860":
version "1.2.1-dev-1650573289860"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1650573289860.tgz#7685e4add2985e95fd945f6e7154f6ecb175d565"
integrity sha512-RpLR/uM/HfEEFtlZmeImT295ohpSCOKTrWcKVXj8UT6L4jj+FJ0SpcW3Fnr2Q5kOulQVL3qXx5t79Nz02O4qvw==
dependencies:
"@babel/polyfill" "^7.8.7"
"@babel/runtime" "^7.9.6"
@@ -1322,8 +1322,8 @@
is-valid-path "^0.1.1"
joi-objectid "^3.0.1"
meant "^1.0.1"
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-telemetry "0.0.11-dev-1649181615374"
medusa-core-utils "1.1.31-dev-1650573289860"
medusa-telemetry "0.0.11-dev-1650573289860"
netrc-parser "^3.1.6"
open "^8.0.6"
ora "^5.4.1"
@@ -1337,13 +1337,13 @@
winston "^3.3.3"
yargs "^15.3.1"
"@medusajs/medusa@1.2.1-dev-1649181615374":
version "1.2.1-dev-1649181615374"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1649181615374.tgz#6a62f8628b84b47a8717e9e0c276f3a9c2e376ce"
integrity sha512-eiCGE6JqYuP7GCzTBGg5LI9U0uQ0wlsR+NuMZVEwldj+xc7qwMjBJwUA7gc58gBv6JesfMYj3VZmJComN4+7Bg==
"@medusajs/medusa@1.2.1-dev-1650573289860":
version "1.2.1-dev-1650573289860"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1650573289860.tgz#740d20bf349be9ad9e0f151305a75bd40284b641"
integrity sha512-kU3l95SjU/B4SQLhof2obdXbWwkMcRKDnSXE2CB4G1a9jchwnzfRDVDy2MjO/4/0l6AgTLHwxoBR8F9zYUsiDQ==
dependencies:
"@hapi/joi" "^16.1.8"
"@medusajs/medusa-cli" "1.2.1-dev-1649181615374"
"@medusajs/medusa-cli" "1.2.1-dev-1650573289860"
"@types/lodash" "^4.14.168"
awilix "^4.2.3"
body-parser "^1.19.0"
@@ -1356,7 +1356,6 @@
core-js "^3.6.5"
cors "^2.8.5"
cross-spawn "^7.0.3"
dotenv "^8.2.0"
express "^4.17.1"
express-session "^1.17.1"
fs-exists-cached "^1.0.0"
@@ -1367,8 +1366,8 @@
joi "^17.3.0"
joi-objectid "^3.0.1"
jsonwebtoken "^8.5.1"
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-test-utils "1.1.37-dev-1649181615374"
medusa-core-utils "1.1.31-dev-1650573289860"
medusa-test-utils "1.1.37-dev-1650573289860"
morgan "^1.9.1"
multer "^1.4.2"
passport "^0.4.0"
@@ -2010,10 +2009,10 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
babel-preset-medusa-package@1.1.19-dev-1649181615374:
version "1.1.19-dev-1649181615374"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1649181615374.tgz#2f13d52fedd336ad4b4c0602b3bf4696d2d08db7"
integrity sha512-N4XL7rTmNM2W+iRR92xvU4bKadP25lY5QR3vndxTxsLNSgcR5tLjKLO/4j7AqiFvcthbE8cF1TcdECH5aJfSuA==
babel-preset-medusa-package@1.1.19-dev-1650573289860:
version "1.1.19-dev-1650573289860"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1650573289860.tgz#493490de8ca1ce75b30545f19fdb9544b6324af4"
integrity sha512-++eqULlSbdH4bnwi/edLa097io4sxZvJSaXIwSC0it7GDB3IXITk5xyuxgKCJue3psSexwuUV58i/or8sZ1idg==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.12.1"
"@babel/plugin-proposal-decorators" "^7.12.1"
@@ -5156,25 +5155,23 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
medusa-core-utils@1.1.31-dev-1649181615374:
version "1.1.31-dev-1649181615374"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1649181615374.tgz#60416bba53eaba607d77ca36789aa23756b8db0f"
integrity sha512-w5nusocZweIrAFJ6sl4hD/mN+UtNjz39IIfXukekyJByg3wpv4P+vsW3XdrFTX5OLvKVxuzjl3B2zeZUmdgSKg==
medusa-core-utils@1.1.31-dev-1650573289860:
version "1.1.31-dev-1650573289860"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1650573289860.tgz#4b6ce1ab888a1b56dc08657e1c9ec0686678d7c1"
integrity sha512-y4Xy9Z+LQAXK4CzGzrC+sn0ngTfZgzIbVTUFVi2YhjRrjCXP36Caisf7e5gEkvmC8TxFsl061mneTcZBX+ni9g==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-interfaces@1.2.1-dev-1649181615374:
version "1.2.1-dev-1649181615374"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1649181615374.tgz#0b664f4e3e8e61b67108a41c8f0f9dd58a947075"
integrity sha512-JRD773nZnxjn/2oNrgb/zXn+scBoNHpW97YQYW7+LFX1JBYafzyGQW3vWTFX4X+q08ehz4dc21CgoMeYco8yvQ==
dependencies:
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-interfaces@1.2.1-dev-1650573289860:
version "1.2.1-dev-1650573289860"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1650573289860.tgz#088ef6571cf3ec4b77de716bb6a3897f5a7a3de7"
integrity sha512-/WFMXz6iZp8tau6V/eVYao4SoIyYDrIUKXx32dfFibsQdnf8ev2CL08iTncfmWgAdlHNlO3lMJKF4arEdv3QTQ==
medusa-telemetry@0.0.11-dev-1649181615374:
version "0.0.11-dev-1649181615374"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1649181615374.tgz#3f4c366ea8d0d0fdde9b289f7e771bb27adae56d"
integrity sha512-RMJR3/qlTb1nV05RnBnX1bNOvYyeuXf4owxLlfbWzKAZirWQ5LAC2GikEGGbHGbw7UgiLgQtu4Rnmg2Uye+VcA==
medusa-telemetry@0.0.11-dev-1650573289860:
version "0.0.11-dev-1650573289860"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1650573289860.tgz#d74c00da87adc4e0105047db519dcbbddc36475e"
integrity sha512-UFOGj3hKpfJLKIaQMZQqb2DlGs0gScPJdrgMa5GQFwOGxcYCN2D6gyWtIWuKEDZ8oG4X8MCE0zTa3r+Sh4+zPQ==
dependencies:
axios "^0.21.1"
axios-retry "^3.1.9"
@@ -5186,13 +5183,13 @@ medusa-telemetry@0.0.11-dev-1649181615374:
remove-trailing-slash "^0.1.1"
uuid "^8.3.2"
medusa-test-utils@1.1.37-dev-1649181615374:
version "1.1.37-dev-1649181615374"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1649181615374.tgz#079c16a791d47c52072c6f0837d0a827208bc9cc"
integrity sha512-hj3iNZsIA01l7qAZrOgt+kT8PDkXKoW4CEL3bhVfIUEwsdv9jID7FGdTIN/7G3diioTypvrVcqRrp0uiWdgp+Q==
medusa-test-utils@1.1.37-dev-1650573289860:
version "1.1.37-dev-1650573289860"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1650573289860.tgz#1c8705617b64c4a474891f99985044faa5f8fa2f"
integrity sha512-MnKhy7hbNcZdYjVm9B3Z9MAT7MTf4oTdqQR9/lmb4qd7dJqd2AbxhglsOn5ZRYQHynuzubS+j9EHkxgOgji8MQ==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-core-utils "1.1.31-dev-1650573289860"
randomatic "^3.1.1"
merge-descriptors@1.0.1:
@@ -1,5 +1,6 @@
import fs from "fs"
import aws from "aws-sdk"
import { parse } from "path"
import { FileService } from "medusa-interfaces"
class DigitalOceanService extends FileService {
@@ -23,12 +24,14 @@ class DigitalOceanService extends FileService {
endpoint: this.endpoint_,
})
const parsedFilename = parse(file.originalname)
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
const s3 = new aws.S3()
var params = {
ACL: "public-read",
Bucket: this.bucket_,
Body: fs.createReadStream(file.path),
Key: `${file.originalname}`,
Key: fileKey,
}
return new Promise((resolve, reject) => {
@@ -1,5 +1,5 @@
import { Type } from "class-transformer"
import { omit } from "lodash"
import { omit, pickBy } from "lodash"
import {
IsArray,
IsBoolean,
@@ -19,6 +19,9 @@ import {
defaultAdminProductRelations,
} from "../products"
import listAndCount from "../../../../controllers/products/admin-list-products"
import { MedusaError } from "medusa-core-utils"
import { getListConfig } from "../../../../utils/get-query-config"
import PriceListService from "../../../../services/price-list"
/**
* @oas [get] /price-lists/:id/products
@@ -78,7 +81,7 @@ export default async (req, res) => {
req.query.price_list_id = [id]
const filterableFields: FilterableProductProps = omit(req.query, [
const query: FilterableProductProps = omit(req.query, [
"limit",
"offset",
"expand",
@@ -86,23 +89,71 @@ export default async (req, res) => {
"order",
])
const result = await listAndCount(
req.scope,
filterableFields,
{},
{
limit: validatedParams.limit ?? 50,
offset: validatedParams.offset ?? 0,
expand: validatedParams.expand,
fields: validatedParams.fields,
order: validatedParams.order,
allowedFields: allowedAdminProductFields,
defaultFields: defaultAdminProductFields as (keyof Product)[],
defaultRelations: defaultAdminProductRelations,
}
const limit = validatedParams.limit ?? 50
const offset = validatedParams.offset ?? 0
const expand = validatedParams.expand
const fields = validatedParams.fields
const order = validatedParams.order
const allowedFields = allowedAdminProductFields
const defaultFields = defaultAdminProductFields as (keyof Product)[]
const defaultRelations = defaultAdminProductRelations.filter(
(r) => r !== "variants.prices"
)
res.json(result)
const priceListService: PriceListService =
req.scope.resolve("priceListService")
let includeFields: (keyof Product)[] | undefined
if (fields) {
includeFields = fields.split(",") as (keyof Product)[]
}
let expandFields: string[] | undefined
if (expand) {
expandFields = expand.split(",")
}
let orderBy: { [k: symbol]: "DESC" | "ASC" } | undefined
if (typeof order !== "undefined") {
let orderField = order
if (order.startsWith("-")) {
const [, field] = order.split("-")
orderField = field
orderBy = { [field]: "DESC" }
} else {
orderBy = { [order]: "ASC" }
}
if (!(allowedFields || []).includes(orderField)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Order field must be a valid product field"
)
}
}
const listConfig = getListConfig<Product>(
defaultFields ?? [],
defaultRelations ?? [],
includeFields,
expandFields,
limit,
offset,
orderBy
)
const [products, count] = await priceListService.listProducts(
id,
pickBy(query, (val) => typeof val !== "undefined"),
listConfig
)
res.json({
products,
count,
offset,
limit,
})
}
enum ProductStatus {
@@ -1,12 +1,15 @@
import BaseService from "../base-service"
import { In, Not } from "typeorm"
import { EntityManager } from "typeorm"
import { MockManager } from "medusa-test-utils"
import { TransactionBaseService } from "../transaction-base-service"
describe("BaseService", () => {
describe("TransactionBaseService", () => {
it("should cloned the child class withTransaction", () => {
class Child extends BaseService<Child> {
class Child extends TransactionBaseService<Child> {
protected manager_!: EntityManager
protected transactionManager_!: EntityManager
constructor(protected readonly container) {
super(container, {});
super(container);
this.container = container
}
@@ -32,30 +35,4 @@ describe("BaseService", () => {
expect(child2.getTransactionManager()).toBeTruthy()
expect((child2.getTransactionManager() as any)?.testProp).toBe('testProp')
})
describe("buildQuery_", () => {
const baseService = new BaseService({}, {})
it("successfully creates query", () => {
const q = baseService.buildQuery_(
{
id: "1234",
test1: ["123", "12", "1"],
test2: Not("this"),
},
{
relations: ["1234"],
}
)
expect(q).toEqual({
where: {
id: "1234",
test1: In(["123", "12", "1"]),
test2: Not("this"),
},
relations: ["1234"],
})
})
})
})
@@ -1,342 +0,0 @@
import { MedusaError } from "medusa-core-utils"
import { EntityManager, FindOperator, In, Raw } from "typeorm"
import { IsolationLevel } from "typeorm/driver/types/IsolationLevel"
import { FindConfig } from "../types/common"
type Selector<TEntity> = { [key in keyof TEntity]?: unknown }
/**
* Common functionality for Services
* @interface
*/
class BaseService<
TChild extends BaseService<TChild, TContainer>,
TContainer = unknown
> {
protected transactionManager_: EntityManager | undefined
protected manager_: EntityManager
private readonly container_: TContainer
constructor(
container: TContainer,
protected readonly configModule: Record<string, unknown>
) {
this.container_ = container
}
withTransaction(): this
withTransaction(transactionManager: EntityManager): TChild
withTransaction(transactionManager?: EntityManager): this | TChild {
if (!transactionManager) {
return this
}
const cloned = new (<typeof BaseService>this.constructor)<
TChild,
TContainer
>(
{
...this.container_,
manager: transactionManager,
},
this.configModule
)
cloned.transactionManager_ = transactionManager
return cloned as TChild
}
/**
* Used to build TypeORM queries.
* @param selector The selector
* @param config The config
* @return The QueryBuilderConfig
*/
buildQuery_<TEntity = unknown>(
selector: Selector<TEntity>,
config: FindConfig<TEntity> = {}
): FindConfig<TEntity> & {
where: { [key in keyof TEntity]?: unknown }
withDeleted?: boolean
} {
const build = (
obj: Record<string, unknown>
): { [key in keyof TEntity]?: unknown } => {
return Object.entries(obj).reduce((acc, [key, value]: any) => {
// Undefined values indicate that they have no significance to the query.
// If the query is looking for rows where a column is not set it should use null instead of undefined
if (typeof value === "undefined") {
return acc
}
const subquery: {
operator: "<" | ">" | "<=" | ">="
value: unknown
}[] = []
switch (true) {
case value instanceof FindOperator:
acc[key] = value
break
case Array.isArray(value):
acc[key] = In([...(value as unknown[])])
break
case value !== null && typeof value === "object":
Object.entries(value as Record<string, unknown>).map(
([modifier, val]) => {
switch (modifier) {
case "lt":
subquery.push({ operator: "<", value: val })
break
case "gt":
subquery.push({ operator: ">", value: val })
break
case "lte":
subquery.push({ operator: "<=", value: val })
break
case "gte":
subquery.push({ operator: ">=", value: val })
break
default:
acc[key] = value
break
}
}
)
if (subquery.length) {
acc[key] = Raw(
(a) =>
subquery
.map((s, index) => `${a} ${s.operator} :${index}`)
.join(" AND "),
subquery.map((s) => s.value)
)
}
break
default:
acc[key] = value
break
}
return acc
}, {} as { [key in keyof TEntity]?: unknown })
}
const query: FindConfig<TEntity> & {
where: { [key in keyof TEntity]?: unknown }
withDeleted?: boolean
} = {
where: build(selector),
}
if ("deleted_at" in selector) {
query.withDeleted = true
}
if ("skip" in config) {
query.skip = config.skip
}
if ("take" in config) {
query.take = config.take
}
if ("relations" in config) {
query.relations = config.relations
}
if ("select" in config) {
query.select = config.select
}
if ("order" in config) {
query.order = config.order
}
return query
}
/**
* Confirms whether a given raw id is valid. Fails if the provided
* id is null or undefined. The validate function takes an optional config
* param, to support checking id prefix and length.
* @param rawId - the id to validate.
* @param config - optional config
* @returns the rawId given that nothing failed
*/
validateId_(
rawId: string,
config: { prefix?: string; length?: number } = {}
): string {
const { prefix, length } = config
if (!rawId) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Failed to validate id: ${rawId}`
)
}
if (prefix || length) {
const [pre, rand] = rawId.split("_")
if (prefix && pre !== prefix) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}`
)
}
if (length && length !== rand.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provided id: ${rawId} does not adhere to length constraint: ${length}`
)
}
}
return rawId
}
shouldRetryTransaction(
err: { code: string } | Record<string, unknown>
): boolean {
if (!(err as { code: string })?.code) {
return false
}
const code = (err as { code: string })?.code
return code === "40001" || code === "40P01"
}
/**
* Wraps some work within a transactional block. If the service already has
* a transaction manager attached this will be reused, otherwise a new
* transaction manager is created.
* @param work - the transactional work to be done
* @param isolationOrErrorHandler - the isolation level to be used for the work.
* @param maybeErrorHandlerOrDontFail Potential error handler
* @return the result of the transactional work
*/
async atomicPhase_(
work: (transactionManager: EntityManager) => Promise<unknown>,
isolationOrErrorHandler?:
| IsolationLevel
| ((error: unknown) => Promise<unknown>),
maybeErrorHandlerOrDontFail?: (error: unknown) => Promise<unknown>
): Promise<unknown | never> {
let errorHandler = maybeErrorHandlerOrDontFail
let isolation:
| IsolationLevel
| ((error: unknown) => Promise<unknown>)
| undefined
| null = isolationOrErrorHandler
let dontFail = false
if (typeof isolationOrErrorHandler === "function") {
isolation = null
errorHandler = isolationOrErrorHandler
dontFail = !!maybeErrorHandlerOrDontFail
}
if (this.transactionManager_) {
const doWork = async (m: EntityManager): Promise<unknown | never> => {
this.manager_ = m
this.transactionManager_ = m
try {
return await work(m)
} catch (error) {
if (errorHandler) {
const queryRunner = this.transactionManager_.queryRunner
if (queryRunner && queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction()
}
await errorHandler(error)
}
throw error
}
}
return doWork(this.transactionManager_)
} else {
const temp = this.manager_
const doWork = async (m: EntityManager): Promise<unknown | never> => {
this.manager_ = m
this.transactionManager_ = m
try {
const result = await work(m)
this.manager_ = temp
this.transactionManager_ = undefined
return result
} catch (error) {
this.manager_ = temp
this.transactionManager_ = undefined
throw error
}
}
if (isolation) {
let result
try {
result = await this.manager_.transaction(
isolation as IsolationLevel,
(m) => doWork(m)
)
return result
} catch (error) {
if (this.shouldRetryTransaction(error)) {
return this.manager_.transaction(isolation as IsolationLevel, (m) =>
doWork(m)
)
} else {
if (errorHandler) {
await errorHandler(error)
}
throw error
}
}
}
try {
return await this.manager_.transaction((m) => doWork(m))
} catch (error) {
if (errorHandler) {
const result = await errorHandler(error)
if (dontFail) {
return result
}
}
throw error
}
}
}
/**
* Dedicated method to set metadata.
* @param obj - the entity to apply metadata to.
* @param metadata - the metadata to set
* @return resolves to the updated result.
*/
setMetadata_(
obj: { metadata: Record<string, unknown> },
metadata: Record<string, unknown>
): Record<string, unknown> {
const existing = obj.metadata || {}
const newData = {}
for (const [key, value] of Object.entries(metadata)) {
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
newData[key] = value
}
return {
...existing,
...newData,
}
}
}
export default BaseService
+1 -1
View File
@@ -1,4 +1,4 @@
export * from "./tax-calculation-strategy"
export * from "./cart-completion-strategy"
export * from "./tax-service"
export * from "./base-service"
export * from "./transaction-base-service"
@@ -0,0 +1,149 @@
import { EntityManager } from "typeorm"
import { IsolationLevel } from "typeorm/driver/types/IsolationLevel"
export abstract class TransactionBaseService<
TChild extends TransactionBaseService<TChild, TContainer>,
TContainer = unknown
> {
protected abstract manager_: EntityManager
protected abstract transactionManager_: EntityManager | undefined
protected constructor(
protected readonly container: TContainer,
protected readonly configModule?: Record<string, unknown>
) {}
withTransaction(transactionManager?: EntityManager): this | TChild {
if (!transactionManager) {
return this
}
const cloned = new (<any>this.constructor)(
{
...this.container,
manager: transactionManager,
},
this.configModule
)
cloned.transactionManager_ = transactionManager
return cloned as TChild
}
protected shouldRetryTransaction_(
err: { code: string } | Record<string, unknown>
): boolean {
if (!(err as { code: string })?.code) {
return false
}
const code = (err as { code: string })?.code
return code === "40001" || code === "40P01"
}
/**
* Wraps some work within a transactional block. If the service already has
* a transaction manager attached this will be reused, otherwise a new
* transaction manager is created.
* @param work - the transactional work to be done
* @param isolationOrErrorHandler - the isolation level to be used for the work.
* @param maybeErrorHandlerOrDontFail Potential error handler
* @return the result of the transactional work
*/
protected async atomicPhase_<TResult, TError>(
work: (transactionManager: EntityManager) => Promise<TResult | never>,
isolationOrErrorHandler?:
| IsolationLevel
| ((error: TError) => Promise<never | TResult | void>),
maybeErrorHandlerOrDontFail?: (
error: TError
) => Promise<never | TResult | void>
): Promise<never | TResult> {
let errorHandler = maybeErrorHandlerOrDontFail
let isolation:
| IsolationLevel
| ((error: TError) => Promise<never | TResult | void>)
| undefined
| null = isolationOrErrorHandler
let dontFail = false
if (typeof isolationOrErrorHandler === "function") {
isolation = null
errorHandler = isolationOrErrorHandler
dontFail = !!maybeErrorHandlerOrDontFail
}
if (this.transactionManager_) {
const doWork = async (m: EntityManager): Promise<never | TResult> => {
this.manager_ = m
this.transactionManager_ = m
try {
return await work(m)
} catch (error) {
if (errorHandler) {
const queryRunner = this.transactionManager_.queryRunner
if (queryRunner && queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction()
}
await errorHandler(error)
}
throw error
}
}
return await doWork(this.transactionManager_)
} else {
const temp = this.manager_
const doWork = async (m: EntityManager): Promise<never | TResult> => {
this.manager_ = m
this.transactionManager_ = m
try {
const result = await work(m)
this.manager_ = temp
this.transactionManager_ = undefined
return result
} catch (error) {
this.manager_ = temp
this.transactionManager_ = undefined
throw error
}
}
if (isolation && this.manager_) {
let result
try {
result = await this.manager_.transaction(
isolation as IsolationLevel,
(m) => doWork(m)
)
return result
} catch (error) {
if (this.shouldRetryTransaction_(error)) {
return this.manager_.transaction(
isolation as IsolationLevel,
(m): Promise<never | TResult> => doWork(m)
)
} else {
if (errorHandler) {
await errorHandler(error)
}
throw error
}
}
}
try {
return await this.manager_.transaction((m) => doWork(m))
} catch (error) {
if (errorHandler) {
const result = await errorHandler(error)
if (dontFail) {
return result as TResult
}
}
throw error
}
}
}
}
@@ -0,0 +1,61 @@
import { createContainer, asValue } from "awilix"
import { mkdirSync, rmSync, rmdirSync, writeFileSync } from "fs"
import { resolve } from "path"
import Logger from "../logger"
import { registerServices } from "../plugins"
import { MedusaContainer } from "../../types/global"
const distTestTargetDirectorPath = resolve(__dirname, "__pluginsLoaderTest__")
const servicesTestTargetDirectoryPath = resolve(distTestTargetDirectorPath, "services")
const buildServiceTemplate = (name: string) => {
return `
import { BaseService } from "medusa-interfaces"
export default class ${name}Service extends BaseService {}
`
}
describe('plugins loader', () => {
const container = createContainer() as MedusaContainer
const pluginsDetails = {
resolve: resolve(__dirname, "__pluginsLoaderTest__"),
name: `project-plugin`,
id: "fakeId",
options: {},
version: '"fakeVersion',
}
describe("registerServices", function() {
beforeAll(() => {
container.register("logger", asValue(Logger))
mkdirSync(servicesTestTargetDirectoryPath, { mode: "777", recursive: true })
writeFileSync(resolve(servicesTestTargetDirectoryPath, "test.js"), buildServiceTemplate("test"))
writeFileSync(resolve(servicesTestTargetDirectoryPath, "test2.js"), buildServiceTemplate("test2"))
writeFileSync(resolve(servicesTestTargetDirectoryPath, "test2.js.map"), "map:file")
writeFileSync(resolve(servicesTestTargetDirectoryPath, "test2.d.ts"), "export interface Test {}")
})
afterAll(() => {
rmSync(distTestTargetDirectorPath, { recursive: true, force: true })
jest.clearAllMocks()
})
it('should load the services from the services directory but only js files', async () => {
let err;
try {
await registerServices(pluginsDetails, container)
} catch (e) {
err = e
}
expect(err).toBeFalsy()
const testService: (...args: unknown[]) => any = container.resolve("testService")
const test2Service: (...args: unknown[]) => any = container.resolve("test2Service")
expect(testService).toBeTruthy()
expect(testService.constructor.name).toBe("testService")
expect(test2Service).toBeTruthy()
expect(test2Service.constructor.name).toBe("test2Service")
})
})
})
+2 -2
View File
@@ -245,8 +245,8 @@ function registerApi(
* registered
* @return {void}
*/
async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise<void> {
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {})
export async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise<void> {
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*.js`, {})
await Promise.all(
files.map(async (fn) => {
const loaded = require(fn).default
@@ -116,6 +116,24 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
.execute()
}
public async findManyForVariantInPriceList(
variant_id: string,
price_list_id: string
): Promise<[MoneyAmount[], number]> {
const qb = this.createQueryBuilder("ma")
.leftJoinAndSelect("ma.price_list", "price_list")
.where("ma.variant_id = :variant_id", { variant_id })
.andWhere(
new Brackets((qb) => {
qb.where("ma.price_list_id = :price_list_id", {
price_list_id,
}).orWhere("ma.price_list_id IS NULL")
})
)
return await qb.getManyAndCount()
}
public async findManyForVariantInRegion(
variant_id: string,
region_id?: string,
@@ -126,21 +144,17 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
const date = new Date()
const qb = this.createQueryBuilder("ma")
.leftJoinAndSelect(
"ma.price_list",
"price_list",
"ma.price_list_id = price_list.id "
)
.leftJoinAndSelect("ma.price_list", "price_list")
.where({ variant_id: variant_id })
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
.andWhere(
"(price_list is null or price_list.ends_at is null OR price_list.ends_at > :date) ",
"(price_list.ends_at is null OR price_list.ends_at > :date)",
{
date: date.toUTCString(),
}
)
.andWhere(
"(price_list is null or price_list.starts_at is null OR price_list.starts_at < :date)",
"(price_list.starts_at is null OR price_list.starts_at < :date)",
{
date: date.toUTCString(),
}
@@ -155,23 +169,19 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
)
)
} else if (!customer_id && !include_discount_prices) {
qb.andWhere("price_list IS null")
qb.andWhere("price_list.id IS null")
}
if (customer_id) {
qb.leftJoin("price_list.customer_groups", "cgroup")
.leftJoin(
"customer_group_customers",
"cgc",
"cgc.customer_group_id = cgroup.id"
)
.andWhere("(cgc is null OR cgc.customer_id = :customer_id)", {
.leftJoin("customer_group_customers", "cgc", "cgc.customer_group_id = cgroup.id")
.andWhere("(cgc.customer_group_id is null OR cgc.customer_id = :customer_id)", {
customer_id,
})
} else {
qb.leftJoin("price_list.customer_groups", "cgroup").andWhere(
"cgroup.id is null"
)
qb
.leftJoin("price_list.customer_groups", "cgroup")
.andWhere("cgroup.id is null")
}
return await qb.getManyAndCount()
}
+92 -2
View File
@@ -1,5 +1,95 @@
import { EntityRepository, Repository } from "typeorm"
import { groupBy, map } from "lodash"
import {
Brackets,
EntityRepository,
FindManyOptions, Repository
} from "typeorm"
import { PriceList } from "../models/price-list"
import { CustomFindOptions } from "../types/common"
type PriceListFindOptions = CustomFindOptions<PriceList, 'status' | 'type'>
@EntityRepository(PriceList)
export class PriceListRepository extends Repository<PriceList> {}
export class PriceListRepository extends Repository<PriceList> {
public async getFreeTextSearchResultsAndCount(
q: string,
options: PriceListFindOptions = { where: {} },
relations: (keyof PriceList)[] = []
): Promise<[PriceList[], number]> {
options.where = options.where ?? {}
let qb = this.createQueryBuilder("price_list")
.leftJoinAndSelect("price_list.customer_groups", "customer_group")
.select(["price_list.id"])
.where(options.where)
.andWhere(
new Brackets((qb) => {
qb.where(`price_list.description ILIKE :q`, { q: `%${q}%` })
.orWhere(`price_list.name ILIKE :q`, { q: `%${q}%` })
.orWhere(`customer_group.name ILIKE :q`, { q: `%${q}%` })
})
)
.skip(options.skip)
.take(options.take)
const [results, count] = await qb.getManyAndCount()
const price_lists = await this.findWithRelations(
relations,
results.map((r) => r.id)
)
return [price_lists, count]
}
public async findWithRelations(
relations: (keyof PriceList)[] = [],
idsOrOptionsWithoutRelations:
| Omit<FindManyOptions<PriceList>, "relations">
| string[] = {}
): Promise<PriceList[]> {
let entities
if (Array.isArray(idsOrOptionsWithoutRelations)) {
entities = await this.findByIds(idsOrOptionsWithoutRelations)
} else {
entities = await this.find(idsOrOptionsWithoutRelations)
}
const groupedRelations: Record<string, string[]> = {}
for (const relation of relations) {
const [topLevel] = relation.split(".")
if (groupedRelations[topLevel]) {
groupedRelations[topLevel].push(relation)
} else {
groupedRelations[topLevel] = [relation]
}
}
const entitiesIds = entities.map(({ id }) => id)
const entitiesIdsWithRelations = await Promise.all(
Object.values(groupedRelations).map((relations: string[]) => {
return this.findByIds(entitiesIds, {
select: ["id"],
relations: relations as string[],
})
})
).then(entitiesIdsWithRelations => entitiesIdsWithRelations.flat())
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entitiesAndRelationsById, (entityAndRelations) =>
this.merge(this.create(), ...entityAndRelations)
)
}
public async findOneWithRelations(
relations: (keyof PriceList)[] = [],
options: Omit<FindManyOptions<PriceList>, "relations"> = {}
): Promise<PriceList | undefined> {
options.take = 1
return (await this.findWithRelations(
relations,
options
))?.pop()
}
}
@@ -3,7 +3,9 @@ import LineItemService from "../line-item"
describe("LineItemService", () => {
describe("create", () => {
const lineItemRepository = MockRepository({})
const lineItemRepository = MockRepository({
create: (data) => data
})
const cartRepository = MockRepository({
findOne: () =>
@@ -105,9 +107,10 @@ describe("LineItemService", () => {
})
it("successfully create a line item giftcard", async () => {
const line = await await lineItemService.generate(
const line = await lineItemService.generate(
IdMap.getId("test-giftcard"),
IdMap.getId("test-region")
IdMap.getId("test-region"),
1
)
await lineItemService.create({
@@ -115,8 +118,8 @@ describe("LineItemService", () => {
cart_id: IdMap.getId("test-cart"),
})
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
expect(lineItemRepository.create).toHaveBeenCalledWith({
expect(lineItemRepository.create).toHaveBeenCalledTimes(2)
expect(lineItemRepository.create).toHaveBeenNthCalledWith(2, expect.objectContaining({
allow_discounts: false,
variant_id: IdMap.getId("test-giftcard"),
cart_id: IdMap.getId("test-cart"),
@@ -128,7 +131,7 @@ describe("LineItemService", () => {
is_giftcard: true,
should_merge: true,
metadata: {},
})
}))
})
})
+42 -64
View File
@@ -1,6 +1,5 @@
import _ from "lodash"
import { MedusaError, Validator } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { DeepPartial, EntityManager, In } from "typeorm"
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
import { Address } from "../models/address"
@@ -37,7 +36,8 @@ import InventoryService from "./inventory"
import CustomShippingOptionService from "./custom-shipping-option"
import LineItemAdjustmentService from "./line-item-adjustment"
import { LineItemRepository } from "../repositories/line-item"
import { raw } from "express"
import { TransactionBaseService } from "../interfaces"
import { buildQuery, setMetadata, validateId } from "../utils"
type InjectedDependencies = {
manager: EntityManager
@@ -71,14 +71,16 @@ type TotalsConfig = {
/* Provides layer to manipulate carts.
* @implements BaseService
*/
class CartService extends BaseService {
static Events = {
class CartService extends TransactionBaseService<CartService> {
static readonly Events = {
CUSTOMER_UPDATED: "cart.customer_updated",
CREATED: "cart.created",
UPDATED: "cart.updated",
}
protected readonly manager_: EntityManager
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
protected readonly cartRepository_: typeof CartRepository
protected readonly addressRepository_: typeof AddressRepository
@@ -125,7 +127,8 @@ class CartService extends BaseService {
lineItemAdjustmentService,
priceSelectionStrategy,
}: InjectedDependencies) {
super()
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.manager_ = manager
this.shippingMethodRepository_ = shippingMethodRepository
@@ -151,41 +154,6 @@ class CartService extends BaseService {
this.priceSelectionStrategy_ = priceSelectionStrategy
}
withTransaction(transactionManager: EntityManager): CartService {
if (!transactionManager) {
return this
}
const cloned = new CartService({
manager: transactionManager,
taxProviderService: this.taxProviderService_,
cartRepository: this.cartRepository_,
lineItemRepository: this.lineItemRepository_,
eventBusService: this.eventBus_,
paymentProviderService: this.paymentProviderService_,
paymentSessionRepository: this.paymentSessionRepository_,
shippingMethodRepository: this.shippingMethodRepository_,
productService: this.productService_,
productVariantService: this.productVariantService_,
regionService: this.regionService_,
lineItemService: this.lineItemService_,
shippingOptionService: this.shippingOptionService_,
customerService: this.customerService_,
discountService: this.discountService_,
totalsService: this.totalsService_,
addressRepository: this.addressRepository_,
giftCardService: this.giftCardService_,
inventoryService: this.inventoryService_,
customShippingOptionService: this.customShippingOptionService_,
lineItemAdjustmentService: this.lineItemAdjustmentService_,
priceSelectionStrategy: this.priceSelectionStrategy_,
})
cloned.transactionManager_ = transactionManager
return cloned
}
protected transformQueryForTotals_(
config: FindConfig<Cart>
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
@@ -293,7 +261,7 @@ class CartService extends BaseService {
this.cartRepository_
)
const query = this.buildQuery_(selector, config)
const query = buildQuery<Cart>(selector, config)
return await cartRepo.find(query)
}
)
@@ -316,12 +284,12 @@ class CartService extends BaseService {
const cartRepo = transactionManager.getCustomRepository(
this.cartRepository_
)
const validatedId = this.validateId_(cartId)
const validatedId = validateId(cartId)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals_(options)
const query = this.buildQuery_(
const query = buildQuery<Cart>(
{ id: validatedId },
{ ...options, select, relations }
)
@@ -706,16 +674,20 @@ class CartService extends BaseService {
* @param shouldAdd - flag to indicate, if we should add or remove
* @return void
*/
async adjustFreeShipping_(cart: Cart, shouldAdd: boolean): Promise<void> {
protected async adjustFreeShipping_(
cart: Cart,
shouldAdd: boolean
): Promise<void> {
const transactionManager = this.transactionManager_ ?? this.manager_
if (cart.shipping_methods?.length) {
const shippingMethodRepository =
this.transactionManager_.getCustomRepository(
this.shippingMethodRepository_
)
const shippingMethodRepository = transactionManager.getCustomRepository(
this.shippingMethodRepository_
)
// if any free shipping discounts, we ensure to update shipping method amount
if (shouldAdd) {
return shippingMethodRepository.update(
await shippingMethodRepository.update(
{
id: In(
cart.shipping_methods.map((shippingMethod) => shippingMethod.id)
@@ -847,8 +819,8 @@ class CartService extends BaseService {
)
}
if ("metadata" in data) {
cart.metadata = this.setMetadata_(cart, data.metadata)
if (data?.metadata) {
cart.metadata = setMetadata(cart, data.metadata)
}
if ("context" in data) {
@@ -1344,7 +1316,7 @@ class CartService extends BaseService {
* @param cartOrCartId - the id of the cart to set payment session for
* @return the result of the update operation.
*/
async setPaymentSessions(cartOrCartId: Cart | string): Promise<Cart> {
async setPaymentSessions(cartOrCartId: Cart | string): Promise<void> {
return await this.atomicPhase_(
async (transactionManager: EntityManager) => {
const psRepo = transactionManager.getCustomRepository(
@@ -1671,6 +1643,8 @@ class CartService extends BaseService {
regionId?: string,
customer_id?: string
): Promise<void> {
const transactionManager = this.transactionManager_ ?? this.manager_
// If the cart contains items, we update the price of the items
// to match the updated region or customer id (keeping the old
// value if it exists)
@@ -1685,7 +1659,7 @@ class CartService extends BaseService {
await Promise.all(
cart.items.map(async (item) => {
const availablePrice = await this.priceSelectionStrategy_
.withTransaction(this.transactionManager_)
.withTransaction(transactionManager)
.calculateVariantPrice(item.variant_id, {
region_id: region.id,
currency_code: region.currency_code,
@@ -1700,14 +1674,14 @@ class CartService extends BaseService {
availablePrice.calculatedPrice !== null
) {
return this.lineItemService_
.withTransaction(this.transactionManager_)
.withTransaction(transactionManager)
.update(item.id, {
has_shipping: false,
unit_price: availablePrice.calculatedPrice,
})
} else {
await this.lineItemService_
.withTransaction(this.transactionManager_)
.withTransaction(transactionManager)
.delete(item.id)
return
}
@@ -1729,6 +1703,8 @@ class CartService extends BaseService {
regionId: string,
countryCode: string | null
): Promise<void> {
const transactionManager = this.transactionManager_ ?? this.manager_
if (cart.completed_at || cart.payment_authorized_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
@@ -1742,7 +1718,7 @@ class CartService extends BaseService {
.retrieve(regionId, {
relations: ["countries"],
})
const addrRepo = this.transactionManager_.getCustomRepository(
const addrRepo = transactionManager.getCustomRepository(
this.addressRepository_
)
cart.region = region
@@ -1820,7 +1796,7 @@ class CartService extends BaseService {
// new shipping method
if (cart.shipping_methods && cart.shipping_methods.length) {
await this.shippingOptionService_
.withTransaction(this.transactionManager_)
.withTransaction(transactionManager)
.deleteShippingMethods(cart.shipping_methods)
}
@@ -1833,7 +1809,7 @@ class CartService extends BaseService {
cart.gift_cards = []
if (cart.payment_sessions && cart.payment_sessions.length) {
const paymentSessionRepo = this.transactionManager_.getCustomRepository(
const paymentSessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
await paymentSessionRepo.delete({
@@ -1851,7 +1827,7 @@ class CartService extends BaseService {
* @param cartId - the id of the cart to delete
* @return the deleted cart or undefined if the cart was not found.
*/
async delete(cartId: string): Promise<string> {
async delete(cartId: string): Promise<Cart> {
return await this.atomicPhase_(
async (transactionManager: EntityManager) => {
const cart = await this.retrieve(cartId, {
@@ -1905,7 +1881,7 @@ class CartService extends BaseService {
this.cartRepository_
)
const validatedId = this.validateId_(cartId)
const validatedId = validateId(cartId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
@@ -1964,20 +1940,22 @@ class CartService extends BaseService {
}
protected async refreshAdjustments_(cart: Cart): Promise<void> {
const transactionManager = this.transactionManager_ ?? this.manager_
const nonReturnLineIDs = cart.items
.filter((item) => !item.is_return)
.map((i) => i.id)
// delete all old non return line item adjustments
await this.lineItemAdjustmentService_
.withTransaction(this.transactionManager_)
.withTransaction(transactionManager)
.delete({
item_id: nonReturnLineIDs,
})
// potentially create/update line item adjustments
await this.lineItemAdjustmentService_
.withTransaction(this.transactionManager_)
.withTransaction(transactionManager)
.createAdjustments(cart)
}
@@ -1993,7 +1971,7 @@ class CartService extends BaseService {
const cartRepo = transactionManager.getCustomRepository(
this.cartRepository_
)
const validatedId = this.validateId_(cartId)
const validatedId = validateId(cartId)
if (typeof key !== "string") {
throw new MedusaError(
-282
View File
@@ -1,282 +0,0 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
/**
* Provides layer to manipulate line items.
* @extends BaseService
*/
class LineItemService extends BaseService {
constructor({
manager,
lineItemRepository,
lineItemTaxLineRepository,
productVariantService,
productService,
regionService,
cartRepository,
lineItemAdjustmentService,
}) {
super()
/** @private @const {EntityManager} */
this.manager_ = manager
/** @private @const {LineItemRepository} */
this.lineItemRepository_ = lineItemRepository
/** @private @const {typeof LineItemTaxLineRepository} */
this.itemTaxLineRepo_ = lineItemTaxLineRepository
/** @private @const {ProductVariantService} */
this.productVariantService_ = productVariantService
/** @private @const {ProductService} */
this.productService_ = productService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {CartRepository} */
this.cartRepository_ = cartRepository
this.lineItemAdjustmentService_ = lineItemAdjustmentService
}
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new LineItemService({
manager: transactionManager,
lineItemRepository: this.lineItemRepository_,
lineItemTaxLineRepository: this.itemTaxLineRepo_,
productVariantService: this.productVariantService_,
productService: this.productService_,
regionService: this.regionService_,
cartRepository: this.cartRepository_,
lineItemAdjustmentService: this.lineItemAdjustmentService_,
})
cloned.transactionManager_ = transactionManager
return cloned
}
async list(
selector,
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const liRepo = this.manager_.getCustomRepository(this.lineItemRepository_)
const query = this.buildQuery_(selector, config)
return liRepo.find(query)
}
/**
* Retrieves a line item by its id.
* @param {string} id - the id of the line item to retrieve
* @param {object} config - the config to be used at query building
* @return {LineItem} the line item
*/
async retrieve(id, config = {}) {
const lineItemRepository = this.manager_.getCustomRepository(
this.lineItemRepository_
)
const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config)
const lineItem = await lineItemRepository.findOne(query)
if (!lineItem) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Line item with ${id} was not found`
)
}
return lineItem
}
/**
* Creates return line items for a given cart based on the return items in a
* return.
* @param {string} returnId - the id to generate return items from.
* @param {string} cartId - the cart to assign the return line items to.
* @return {Promise<LineItem[]>} the created line items
*/
async createReturnLines(returnId, cartId) {
const lineItemRepo = this.manager_.getCustomRepository(
this.lineItemRepository_
)
const itemTaxLineRepo = this.manager_.getCustomRepository(
this.itemTaxLineRepo_
)
const items = await lineItemRepo.findByReturn(returnId)
const toCreate = items.map((i) =>
lineItemRepo.create({
cart_id: cartId,
thumbnail: i.thumbnail,
is_return: true,
title: i.title,
variant_id: i.variant_id,
unit_price: -1 * i.unit_price,
quantity: i.return_item.quantity,
allow_discounts: i.allow_discounts,
tax_lines: i.tax_lines.map((tl) => {
return itemTaxLineRepo.create({
name: tl.name,
code: tl.code,
rate: tl.rate,
metadata: tl.metadata,
})
}),
metadata: i.metadata,
adjustments: i.adjustments.map((adjustment) => {
return {
amount: -1 * adjustment.amount,
description: adjustment.description,
discount_id: adjustment.discount_id,
metadata: adjustment.metadata,
}
}),
})
)
return await lineItemRepo.save(toCreate)
}
async generate(variantId, regionId, quantity, context = {}) {
return this.atomicPhase_(async (manager) => {
const variant = await this.productVariantService_
.withTransaction(manager)
.retrieve(variantId, {
relations: ["product"],
include_discount_prices: true,
})
const region = await this.regionService_
.withTransaction(manager)
.retrieve(regionId)
let price
let shouldMerge = true
if (context.unit_price !== undefined && context.unit_price !== null) {
// if custom unit_price, we ensure positive values
// and we choose to not merge the items
shouldMerge = false
if (context.unit_price < 0) {
price = 0
} else {
price = context.unit_price
}
} else {
price = await this.productVariantService_
.withTransaction(manager)
.getRegionPrice(variant.id, {
regionId: region.id,
quantity: quantity,
customer_id: context.customer_id,
include_discount_prices: true,
})
}
const toCreate = {
unit_price: price,
title: variant.product.title,
description: variant.title,
thumbnail: variant.product.thumbnail,
variant_id: variant.id,
quantity: quantity || 1,
allow_discounts: variant.product.discountable,
is_giftcard: variant.product.is_giftcard,
metadata: context?.metadata || {},
should_merge: shouldMerge,
}
if (context.cart) {
const adjustments = await this.lineItemAdjustmentService_
.withTransaction(manager)
.generateAdjustments(context.cart, toCreate, { variant })
toCreate.adjustments = adjustments
}
return toCreate
})
}
/**
* Create a line item
* @param {LineItem} lineItem - the line item object to create
* @return {LineItem} the created line item
*/
async create(lineItem) {
return this.atomicPhase_(async (manager) => {
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
const created = await lineItemRepository.create(lineItem)
const result = await lineItemRepository.save(created)
return result
})
}
/**
* Updates a line item
* @param {string} id - the id of the line item to update
* @param {object} update - the properties to update on line item
* @return {LineItem} the update line item
*/
async update(id, update) {
return this.atomicPhase_(async (manager) => {
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
const lineItem = await this.retrieve(id)
const { metadata, ...rest } = update
if (metadata) {
lineItem.metadata = this.setMetadata_(lineItem, metadata)
}
for (const [key, value] of Object.entries(rest)) {
lineItem[key] = value
}
const result = await lineItemRepository.save(lineItem)
return result
})
}
/**
* Deletes a line item.
* @param {string} id - the id of the line item to delete
* @return {Promise} the result of the delete operation
*/
async delete(id) {
return this.atomicPhase_(async (manager) => {
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
const lineItem = await lineItemRepository.findOne({ where: { id } })
if (!lineItem) {
return Promise.resolve()
}
await lineItemRepository.remove(lineItem)
return Promise.resolve()
})
}
}
export default LineItemService
+2 -2
View File
@@ -236,10 +236,10 @@ class LineItemService extends BaseService {
should_merge: shouldMerge,
}
const lineLitemRepo = transactionManager.getCustomRepository(
const lineItemRepo = transactionManager.getCustomRepository(
this.lineItemRepository_
)
const lineItem = lineLitemRepo.create(rawLineItem)
const lineItem = lineItemRepo.create(rawLineItem)
if (context.cart) {
const adjustments = await this.lineItemAdjustmentService_
+62 -2
View File
@@ -2,6 +2,7 @@ import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { EntityManager } from "typeorm"
import { CustomerGroupService } from "."
import { Product } from "../models"
import { CustomerGroup } from "../models/customer-group"
import { PriceList } from "../models/price-list"
import { MoneyAmountRepository } from "../repositories/money-amount"
@@ -14,10 +15,12 @@ import {
UpdatePriceListInput,
} from "../types/price-list"
import { formatException } from "../utils/exception-formatter"
import ProductService from "./product"
type PriceListConstructorProps = {
manager: EntityManager
customerGroupService: CustomerGroupService
productService: ProductService
priceListRepository: typeof PriceListRepository
moneyAmountRepository: typeof MoneyAmountRepository
}
@@ -29,18 +32,21 @@ type PriceListConstructorProps = {
class PriceListService extends BaseService {
private manager_: EntityManager
private customerGroupService_: CustomerGroupService
private productService_: ProductService
private priceListRepo_: typeof PriceListRepository
private moneyAmountRepo_: typeof MoneyAmountRepository
constructor({
manager,
customerGroupService,
productService,
priceListRepository,
moneyAmountRepository,
}: PriceListConstructorProps) {
super()
this.manager_ = manager
this.customerGroupService_ = customerGroupService
this.productService_ = productService
this.priceListRepo_ = priceListRepository
this.moneyAmountRepo_ = moneyAmountRepository
}
@@ -53,6 +59,7 @@ class PriceListService extends BaseService {
const cloned = new PriceListService({
manager: transactionManager,
customerGroupService: this.customerGroupService_,
productService: this.productService_,
priceListRepository: this.priceListRepo_,
moneyAmountRepository: this.moneyAmountRepo_,
})
@@ -253,9 +260,18 @@ class PriceListService extends BaseService {
config: FindConfig<PriceList> = { skip: 0, take: 20 }
): Promise<[PriceList[], number]> {
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
const q = selector.q
const { relations, ...query } = this.buildQuery_(selector, config)
const query = this.buildQuery_(selector, config)
return await priceListRepo.findAndCount(query)
if (q) {
delete query.where.q
return await priceListRepo.getFreeTextSearchResultsAndCount(
q,
query,
relations
)
}
return await priceListRepo.findAndCount({ ...query, relations })
}
async upsertCustomerGroups_(
@@ -276,6 +292,50 @@ class PriceListService extends BaseService {
await priceListRepo.save(priceList)
}
async listProducts(
priceListId: string,
selector = {},
config: FindConfig<Product> = {
relations: [],
skip: 0,
take: 20,
}
): Promise<[Product[], number]> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const [products, count] = await this.productService_.listAndCount(
selector,
config
)
const moneyAmountRepo = manager.getCustomRepository(this.moneyAmountRepo_)
const productsWithPrices = await Promise.all(
products.map(async (p) => {
if (p.variants?.length) {
p.variants = await Promise.all(
p.variants.map(async (v) => {
const [prices] =
await moneyAmountRepo.findManyForVariantInPriceList(
v.id,
priceListId
)
return {
...v,
prices,
}
})
)
}
return p
})
)
return [productsWithPrices, count]
})
}
}
export default PriceListService
@@ -751,7 +751,7 @@ class ProductVariantService extends BaseService {
const variant = await variantRepo.findOne({
where: { id: variantId },
relations: ["prices"],
relations: ["prices", "options"],
})
if (!variant) {
+1 -1
View File
@@ -668,7 +668,7 @@ class ProductService extends BaseService {
// Should not fail, if product does not exist, since delete is idempotent
const product = await productRepo.findOne(
{ id: productId },
{ relations: ["variants"] }
{ relations: ["variants", "variants.prices", "variants.options"] }
)
if (!product) {
+1 -1
View File
@@ -71,5 +71,5 @@ export type CartUpdateProps = {
discounts?: Discount[]
customer_id?: string
context?: object
metadata?: object
metadata?: Record<string, unknown>
}
+33
View File
@@ -1,12 +1,34 @@
import { Transform, Type } from "class-transformer"
import { IsDate, IsNumber, IsOptional, IsString } from "class-validator"
import "reflect-metadata"
import {
BaseEntity,
FindManyOptions,
FindOperator,
OrderByCondition,
} from "typeorm"
import { transformDate } from "../utils/validators/date-transform"
export type PartialPick<T, K extends keyof T> = {
[P in K]?: T[P]
}
export type Writable<T> = { -readonly [key in keyof T]: T[key] }
export type ExtendedFindConfig<TEntity> = FindConfig<TEntity> & {
where: Partial<Writable<TEntity>>
withDeleted?: boolean
}
export type Selector<TEntity> = {
[key in keyof TEntity]?:
| TEntity[key]
| TEntity[key][]
| DateComparisonOperator
| StringComparisonOperator
| NumericalComparisonOperator
}
export type TotalField =
| "shipping_total"
| "discount_total"
@@ -25,6 +47,17 @@ export interface FindConfig<Entity> {
order?: Record<string, "ASC" | "DESC">
}
export interface CustomFindOptions<TModel, InKeys extends keyof TModel> {
select?: FindManyOptions<TModel>["select"]
where?: FindManyOptions<TModel>["where"] &
{
[P in InKeys]?: TModel[P][]
}
order?: OrderByCondition
skip?: number
take?: number
}
export type PaginatedResponse = { limit: number; offset: number; count: number }
export type DeleteResponse = {
@@ -0,0 +1,28 @@
import { In, Not } from "typeorm"
import { buildQuery } from "../build-query"
describe('buildQuery', () => {
describe("buildQuery_", () => {
it("successfully creates query", () => {
const q = buildQuery(
{
id: "1234",
test1: ["123", "12", "1"],
test2: Not("this"),
},
{
relations: ["1234"],
}
)
expect(q).toEqual({
where: {
id: "1234",
test1: In(["123", "12", "1"]),
test2: Not("this"),
},
relations: ["1234"],
})
})
})
})
+113
View File
@@ -0,0 +1,113 @@
import {
ExtendedFindConfig,
FindConfig,
Selector,
Writable,
} from "../types/common"
import { FindOperator, In, Raw } from "typeorm"
/**
* Used to build TypeORM queries.
* @param selector The selector
* @param config The config
* @return The QueryBuilderConfig
*/
export function buildQuery<TEntity = unknown>(
selector: Selector<TEntity>,
config: FindConfig<TEntity> = {}
): ExtendedFindConfig<TEntity> {
const build = (
obj: Selector<TEntity>
): Partial<Writable<TEntity>> => {
return Object.entries(obj).reduce((acc, [key, value]: any) => {
// Undefined values indicate that they have no significance to the query.
// If the query is looking for rows where a column is not set it should use null instead of undefined
if (typeof value === "undefined") {
return acc
}
const subquery: {
operator: "<" | ">" | "<=" | ">="
value: unknown
}[] = []
switch (true) {
case value instanceof FindOperator:
acc[key] = value
break
case Array.isArray(value):
acc[key] = In([...(value as unknown[])])
break
case value !== null && typeof value === "object":
Object.entries(value).map(([modifier, val]) => {
switch (modifier) {
case "lt":
subquery.push({ operator: "<", value: val })
break
case "gt":
subquery.push({ operator: ">", value: val })
break
case "lte":
subquery.push({ operator: "<=", value: val })
break
case "gte":
subquery.push({ operator: ">=", value: val })
break
default:
acc[key] = value
break
}
})
if (subquery.length) {
acc[key] = Raw(
(a) =>
subquery
.map((s, index) => `${a} ${s.operator} :${index}`)
.join(" AND "),
subquery.map((s) => s.value)
)
}
break
default:
acc[key] = value
break
}
return acc
}, {} as Partial<Writable<TEntity>>)
}
const query: FindConfig<TEntity> & {
where: Partial<Writable<TEntity>>
withDeleted?: boolean
} = {
where: build(selector),
}
if ("deleted_at" in selector) {
query.withDeleted = true
}
if ("skip" in config) {
query.skip = config.skip
}
if ("take" in config) {
query.take = config.take
}
if ("relations" in config) {
query.relations = config.relations
}
if ("select" in config) {
query.select = config.select
}
if ("order" in config) {
query.order = config.order
}
return query
}
+3
View File
@@ -0,0 +1,3 @@
export * from './build-query'
export * from './set-metadata'
export * from './validate-id'
+29
View File
@@ -0,0 +1,29 @@
import { MedusaError } from "medusa-core-utils/dist"
/**
* Dedicated method to set metadata.
* @param obj - the entity to apply metadata to.
* @param metadata - the metadata to set
* @return resolves to the updated result.
*/
export function setMetadata(
obj: { metadata: Record<string, unknown> },
metadata: Record<string, unknown>
): Record<string, unknown> {
const existing = obj.metadata || {}
const newData = {}
for (const [key, value] of Object.entries(metadata)) {
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
newData[key] = value
}
return {
...existing,
...newData,
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Confirms whether a given raw id is valid. Fails if the provided
* id is null or undefined. The validate function takes an optional config
* param, to support checking id prefix and length.
* @param rawId - the id to validate.
* @param config - optional config
* @returns the rawId given that nothing failed
*/
import { MedusaError } from "medusa-core-utils/dist"
export function validateId(
rawId: string,
config: { prefix?: string; length?: number } = {}
): string {
const { prefix, length } = config
if (!rawId) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Failed to validate id: ${rawId}`
)
}
if (prefix || length) {
const [pre, rand] = rawId.split("_")
if (prefix && pre !== prefix) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}`
)
}
if (length && length !== rand.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provided id: ${rawId} does not adhere to length constraint: ${length}`
)
}
}
return rawId
}