Merge branch 'develop' into fix-cartServiceCreateLostShippingAddress
This commit is contained in:
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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+8
-31
@@ -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,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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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: {},
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -71,5 +71,5 @@ export type CartUpdateProps = {
|
||||
discounts?: Discount[]
|
||||
customer_id?: string
|
||||
context?: object
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
@@ -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"],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './build-query'
|
||||
export * from './set-metadata'
|
||||
export * from './validate-id'
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user