Merge branch 'develop' into fix-cartServiceCreateLostShippingAddress
This commit is contained in:
29 files changed
+1177
-819
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", () => {
|
describe("POST /admin/price-lists/:id", () => {
|
||||||
@@ -794,9 +905,17 @@ describe("/admin/price-lists", () => {
|
|||||||
|
|
||||||
await simplePriceListFactory(dbConnection, {
|
await simplePriceListFactory(dbConnection, {
|
||||||
id: "test-list",
|
id: "test-list",
|
||||||
|
customer_groups: ["test-group"],
|
||||||
prices: [
|
prices: [
|
||||||
{ variant_id: "test-variant-1", currency_code: "usd", amount: 100 },
|
{ variant_id: "test-variant-1", currency_code: "usd", amount: 150 },
|
||||||
{ variant_id: "test-variant-4", currency_code: "usd", amount: 100 },
|
{ 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) {
|
} catch (err) {
|
||||||
@@ -810,7 +929,7 @@ describe("/admin/price-lists", () => {
|
|||||||
await db.teardown()
|
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 api = useApi()
|
||||||
|
|
||||||
const response = await api
|
const response = await api
|
||||||
@@ -826,8 +945,50 @@ describe("/admin/price-lists", () => {
|
|||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.count).toEqual(2)
|
expect(response.data.count).toEqual(2)
|
||||||
expect(response.data.products).toEqual([
|
expect(response.data.products).toEqual([
|
||||||
expect.objectContaining({ id: "test-prod-1" }),
|
expect.objectContaining({
|
||||||
expect.objectContaining({ id: "test-prod-2" }),
|
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 adminSeeder = require("../../helpers/admin-seeder")
|
||||||
const productSeeder = require("../../helpers/product-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")
|
const priceListSeeder = require("../../helpers/price-list-seeder")
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
@@ -1737,6 +1737,177 @@ describe("/admin/products", () => {
|
|||||||
expect(variant).toEqual(undefined)
|
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 () => {
|
it("successfully creates product with soft-deleted product handle and deletes it again", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
MoneyAmount,
|
MoneyAmount,
|
||||||
PriceListType,
|
PriceListType,
|
||||||
PriceListStatus,
|
PriceListStatus,
|
||||||
|
CustomerGroup,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import faker from "faker"
|
import faker from "faker"
|
||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
@@ -38,6 +39,28 @@ export const simplePriceListFactory = async (
|
|||||||
const manager = connection.manager
|
const manager = connection.manager
|
||||||
|
|
||||||
const listId = data.id || `simple-price-list-${Math.random() * 1000}`
|
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 = {
|
const toCreate = {
|
||||||
id: listId,
|
id: listId,
|
||||||
name: data.name || faker.commerce.productName(),
|
name: data.name || faker.commerce.productName(),
|
||||||
@@ -46,6 +69,7 @@ export const simplePriceListFactory = async (
|
|||||||
type: data.type || PriceListType.OVERRIDE,
|
type: data.type || PriceListType.OVERRIDE,
|
||||||
starts_at: data.starts_at || null,
|
starts_at: data.starts_at || null,
|
||||||
ends_at: data.ends_at || null,
|
ends_at: data.ends_at || null,
|
||||||
|
customer_groups: customerGroups,
|
||||||
}
|
}
|
||||||
|
|
||||||
const toSave = manager.create(PriceList, toCreate)
|
const toSave = manager.create(PriceList, toCreate)
|
||||||
|
|||||||
@@ -8,16 +8,16 @@
|
|||||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/medusa": "1.2.1-dev-1649181615374",
|
"@medusajs/medusa": "1.2.1-dev-1650573289860",
|
||||||
"faker": "^5.5.3",
|
"faker": "^5.5.3",
|
||||||
"medusa-interfaces": "1.2.1-dev-1649181615374",
|
"medusa-interfaces": "1.2.1-dev-1650573289860",
|
||||||
"typeorm": "^0.2.31"
|
"typeorm": "^0.2.31"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.12.10",
|
"@babel/cli": "^7.12.10",
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/node": "^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"
|
"jest": "^26.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1301,10 +1301,10 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.0.3"
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
"@medusajs/medusa-cli@1.2.1-dev-1649181615374":
|
"@medusajs/medusa-cli@1.2.1-dev-1650573289860":
|
||||||
version "1.2.1-dev-1649181615374"
|
version "1.2.1-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1649181615374.tgz#1ea9014e3ec9813a52457b0d6e2fc6bb64d3bfd6"
|
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1650573289860.tgz#7685e4add2985e95fd945f6e7154f6ecb175d565"
|
||||||
integrity sha512-8m6Z1ZZqstZKaAaKoFS3v3IzI7BFhcBgpF+iCSRuJoXltQgzVQOAxXuPjkRoi+m1ZZ+Yi/YYEzKmNQ99vmXisQ==
|
integrity sha512-RpLR/uM/HfEEFtlZmeImT295ohpSCOKTrWcKVXj8UT6L4jj+FJ0SpcW3Fnr2Q5kOulQVL3qXx5t79Nz02O4qvw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.8.7"
|
"@babel/polyfill" "^7.8.7"
|
||||||
"@babel/runtime" "^7.9.6"
|
"@babel/runtime" "^7.9.6"
|
||||||
@@ -1322,8 +1322,8 @@
|
|||||||
is-valid-path "^0.1.1"
|
is-valid-path "^0.1.1"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
meant "^1.0.1"
|
meant "^1.0.1"
|
||||||
medusa-core-utils "1.1.31-dev-1649181615374"
|
medusa-core-utils "1.1.31-dev-1650573289860"
|
||||||
medusa-telemetry "0.0.11-dev-1649181615374"
|
medusa-telemetry "0.0.11-dev-1650573289860"
|
||||||
netrc-parser "^3.1.6"
|
netrc-parser "^3.1.6"
|
||||||
open "^8.0.6"
|
open "^8.0.6"
|
||||||
ora "^5.4.1"
|
ora "^5.4.1"
|
||||||
@@ -1337,13 +1337,13 @@
|
|||||||
winston "^3.3.3"
|
winston "^3.3.3"
|
||||||
yargs "^15.3.1"
|
yargs "^15.3.1"
|
||||||
|
|
||||||
"@medusajs/medusa@1.2.1-dev-1649181615374":
|
"@medusajs/medusa@1.2.1-dev-1650573289860":
|
||||||
version "1.2.1-dev-1649181615374"
|
version "1.2.1-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1649181615374.tgz#6a62f8628b84b47a8717e9e0c276f3a9c2e376ce"
|
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1650573289860.tgz#740d20bf349be9ad9e0f151305a75bd40284b641"
|
||||||
integrity sha512-eiCGE6JqYuP7GCzTBGg5LI9U0uQ0wlsR+NuMZVEwldj+xc7qwMjBJwUA7gc58gBv6JesfMYj3VZmJComN4+7Bg==
|
integrity sha512-kU3l95SjU/B4SQLhof2obdXbWwkMcRKDnSXE2CB4G1a9jchwnzfRDVDy2MjO/4/0l6AgTLHwxoBR8F9zYUsiDQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hapi/joi" "^16.1.8"
|
"@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"
|
"@types/lodash" "^4.14.168"
|
||||||
awilix "^4.2.3"
|
awilix "^4.2.3"
|
||||||
body-parser "^1.19.0"
|
body-parser "^1.19.0"
|
||||||
@@ -1356,7 +1356,6 @@
|
|||||||
core-js "^3.6.5"
|
core-js "^3.6.5"
|
||||||
cors "^2.8.5"
|
cors "^2.8.5"
|
||||||
cross-spawn "^7.0.3"
|
cross-spawn "^7.0.3"
|
||||||
dotenv "^8.2.0"
|
|
||||||
express "^4.17.1"
|
express "^4.17.1"
|
||||||
express-session "^1.17.1"
|
express-session "^1.17.1"
|
||||||
fs-exists-cached "^1.0.0"
|
fs-exists-cached "^1.0.0"
|
||||||
@@ -1367,8 +1366,8 @@
|
|||||||
joi "^17.3.0"
|
joi "^17.3.0"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
jsonwebtoken "^8.5.1"
|
jsonwebtoken "^8.5.1"
|
||||||
medusa-core-utils "1.1.31-dev-1649181615374"
|
medusa-core-utils "1.1.31-dev-1650573289860"
|
||||||
medusa-test-utils "1.1.37-dev-1649181615374"
|
medusa-test-utils "1.1.37-dev-1650573289860"
|
||||||
morgan "^1.9.1"
|
morgan "^1.9.1"
|
||||||
multer "^1.4.2"
|
multer "^1.4.2"
|
||||||
passport "^0.4.0"
|
passport "^0.4.0"
|
||||||
@@ -2010,10 +2009,10 @@ babel-preset-jest@^26.6.2:
|
|||||||
babel-plugin-jest-hoist "^26.6.2"
|
babel-plugin-jest-hoist "^26.6.2"
|
||||||
babel-preset-current-node-syntax "^1.0.0"
|
babel-preset-current-node-syntax "^1.0.0"
|
||||||
|
|
||||||
babel-preset-medusa-package@1.1.19-dev-1649181615374:
|
babel-preset-medusa-package@1.1.19-dev-1650573289860:
|
||||||
version "1.1.19-dev-1649181615374"
|
version "1.1.19-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1649181615374.tgz#2f13d52fedd336ad4b4c0602b3bf4696d2d08db7"
|
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1650573289860.tgz#493490de8ca1ce75b30545f19fdb9544b6324af4"
|
||||||
integrity sha512-N4XL7rTmNM2W+iRR92xvU4bKadP25lY5QR3vndxTxsLNSgcR5tLjKLO/4j7AqiFvcthbE8cF1TcdECH5aJfSuA==
|
integrity sha512-++eqULlSbdH4bnwi/edLa097io4sxZvJSaXIwSC0it7GDB3IXITk5xyuxgKCJue3psSexwuUV58i/or8sZ1idg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
"@babel/plugin-proposal-class-properties" "^7.12.1"
|
||||||
"@babel/plugin-proposal-decorators" "^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"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
medusa-core-utils@1.1.31-dev-1649181615374:
|
medusa-core-utils@1.1.31-dev-1650573289860:
|
||||||
version "1.1.31-dev-1649181615374"
|
version "1.1.31-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1649181615374.tgz#60416bba53eaba607d77ca36789aa23756b8db0f"
|
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1650573289860.tgz#4b6ce1ab888a1b56dc08657e1c9ec0686678d7c1"
|
||||||
integrity sha512-w5nusocZweIrAFJ6sl4hD/mN+UtNjz39IIfXukekyJByg3wpv4P+vsW3XdrFTX5OLvKVxuzjl3B2zeZUmdgSKg==
|
integrity sha512-y4Xy9Z+LQAXK4CzGzrC+sn0ngTfZgzIbVTUFVi2YhjRrjCXP36Caisf7e5gEkvmC8TxFsl061mneTcZBX+ni9g==
|
||||||
dependencies:
|
dependencies:
|
||||||
joi "^17.3.0"
|
joi "^17.3.0"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
|
|
||||||
medusa-interfaces@1.2.1-dev-1649181615374:
|
medusa-interfaces@1.2.1-dev-1650573289860:
|
||||||
version "1.2.1-dev-1649181615374"
|
version "1.2.1-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1649181615374.tgz#0b664f4e3e8e61b67108a41c8f0f9dd58a947075"
|
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1650573289860.tgz#088ef6571cf3ec4b77de716bb6a3897f5a7a3de7"
|
||||||
integrity sha512-JRD773nZnxjn/2oNrgb/zXn+scBoNHpW97YQYW7+LFX1JBYafzyGQW3vWTFX4X+q08ehz4dc21CgoMeYco8yvQ==
|
integrity sha512-/WFMXz6iZp8tau6V/eVYao4SoIyYDrIUKXx32dfFibsQdnf8ev2CL08iTncfmWgAdlHNlO3lMJKF4arEdv3QTQ==
|
||||||
dependencies:
|
|
||||||
medusa-core-utils "1.1.31-dev-1649181615374"
|
|
||||||
|
|
||||||
medusa-telemetry@0.0.11-dev-1649181615374:
|
medusa-telemetry@0.0.11-dev-1650573289860:
|
||||||
version "0.0.11-dev-1649181615374"
|
version "0.0.11-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1649181615374.tgz#3f4c366ea8d0d0fdde9b289f7e771bb27adae56d"
|
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1650573289860.tgz#d74c00da87adc4e0105047db519dcbbddc36475e"
|
||||||
integrity sha512-RMJR3/qlTb1nV05RnBnX1bNOvYyeuXf4owxLlfbWzKAZirWQ5LAC2GikEGGbHGbw7UgiLgQtu4Rnmg2Uye+VcA==
|
integrity sha512-UFOGj3hKpfJLKIaQMZQqb2DlGs0gScPJdrgMa5GQFwOGxcYCN2D6gyWtIWuKEDZ8oG4X8MCE0zTa3r+Sh4+zPQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
axios "^0.21.1"
|
axios "^0.21.1"
|
||||||
axios-retry "^3.1.9"
|
axios-retry "^3.1.9"
|
||||||
@@ -5186,13 +5183,13 @@ medusa-telemetry@0.0.11-dev-1649181615374:
|
|||||||
remove-trailing-slash "^0.1.1"
|
remove-trailing-slash "^0.1.1"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
medusa-test-utils@1.1.37-dev-1649181615374:
|
medusa-test-utils@1.1.37-dev-1650573289860:
|
||||||
version "1.1.37-dev-1649181615374"
|
version "1.1.37-dev-1650573289860"
|
||||||
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1649181615374.tgz#079c16a791d47c52072c6f0837d0a827208bc9cc"
|
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1650573289860.tgz#1c8705617b64c4a474891f99985044faa5f8fa2f"
|
||||||
integrity sha512-hj3iNZsIA01l7qAZrOgt+kT8PDkXKoW4CEL3bhVfIUEwsdv9jID7FGdTIN/7G3diioTypvrVcqRrp0uiWdgp+Q==
|
integrity sha512-MnKhy7hbNcZdYjVm9B3Z9MAT7MTf4oTdqQR9/lmb4qd7dJqd2AbxhglsOn5ZRYQHynuzubS+j9EHkxgOgji8MQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/plugin-transform-classes" "^7.9.5"
|
"@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"
|
randomatic "^3.1.1"
|
||||||
|
|
||||||
merge-descriptors@1.0.1:
|
merge-descriptors@1.0.1:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import aws from "aws-sdk"
|
import aws from "aws-sdk"
|
||||||
|
import { parse } from "path"
|
||||||
import { FileService } from "medusa-interfaces"
|
import { FileService } from "medusa-interfaces"
|
||||||
|
|
||||||
class DigitalOceanService extends FileService {
|
class DigitalOceanService extends FileService {
|
||||||
@@ -23,12 +24,14 @@ class DigitalOceanService extends FileService {
|
|||||||
endpoint: this.endpoint_,
|
endpoint: this.endpoint_,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const parsedFilename = parse(file.originalname)
|
||||||
|
const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}`
|
||||||
const s3 = new aws.S3()
|
const s3 = new aws.S3()
|
||||||
var params = {
|
var params = {
|
||||||
ACL: "public-read",
|
ACL: "public-read",
|
||||||
Bucket: this.bucket_,
|
Bucket: this.bucket_,
|
||||||
Body: fs.createReadStream(file.path),
|
Body: fs.createReadStream(file.path),
|
||||||
Key: `${file.originalname}`,
|
Key: fileKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import { omit } from "lodash"
|
import { omit, pickBy } from "lodash"
|
||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
defaultAdminProductRelations,
|
defaultAdminProductRelations,
|
||||||
} from "../products"
|
} from "../products"
|
||||||
import listAndCount from "../../../../controllers/products/admin-list-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
|
* @oas [get] /price-lists/:id/products
|
||||||
@@ -78,7 +81,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
req.query.price_list_id = [id]
|
req.query.price_list_id = [id]
|
||||||
|
|
||||||
const filterableFields: FilterableProductProps = omit(req.query, [
|
const query: FilterableProductProps = omit(req.query, [
|
||||||
"limit",
|
"limit",
|
||||||
"offset",
|
"offset",
|
||||||
"expand",
|
"expand",
|
||||||
@@ -86,23 +89,71 @@ export default async (req, res) => {
|
|||||||
"order",
|
"order",
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await listAndCount(
|
const limit = validatedParams.limit ?? 50
|
||||||
req.scope,
|
const offset = validatedParams.offset ?? 0
|
||||||
filterableFields,
|
const expand = validatedParams.expand
|
||||||
{},
|
const fields = validatedParams.fields
|
||||||
{
|
const order = validatedParams.order
|
||||||
limit: validatedParams.limit ?? 50,
|
const allowedFields = allowedAdminProductFields
|
||||||
offset: validatedParams.offset ?? 0,
|
const defaultFields = defaultAdminProductFields as (keyof Product)[]
|
||||||
expand: validatedParams.expand,
|
const defaultRelations = defaultAdminProductRelations.filter(
|
||||||
fields: validatedParams.fields,
|
(r) => r !== "variants.prices"
|
||||||
order: validatedParams.order,
|
|
||||||
allowedFields: allowedAdminProductFields,
|
|
||||||
defaultFields: defaultAdminProductFields as (keyof Product)[],
|
|
||||||
defaultRelations: defaultAdminProductRelations,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
enum ProductStatus {
|
||||||
|
|||||||
+9
-32
@@ -1,12 +1,15 @@
|
|||||||
import BaseService from "../base-service"
|
import { EntityManager } from "typeorm"
|
||||||
import { In, Not } from "typeorm"
|
|
||||||
import { MockManager } from "medusa-test-utils"
|
import { MockManager } from "medusa-test-utils"
|
||||||
|
import { TransactionBaseService } from "../transaction-base-service"
|
||||||
|
|
||||||
describe("BaseService", () => {
|
describe("TransactionBaseService", () => {
|
||||||
it("should cloned the child class withTransaction", () => {
|
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) {
|
constructor(protected readonly container) {
|
||||||
super(container, {});
|
super(container);
|
||||||
this.container = container
|
this.container = container
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,30 +35,4 @@ describe("BaseService", () => {
|
|||||||
expect(child2.getTransactionManager()).toBeTruthy()
|
expect(child2.getTransactionManager()).toBeTruthy()
|
||||||
expect((child2.getTransactionManager() as any)?.testProp).toBe('testProp')
|
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 "./tax-calculation-strategy"
|
||||||
export * from "./cart-completion-strategy"
|
export * from "./cart-completion-strategy"
|
||||||
export * from "./tax-service"
|
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
|
* registered
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise<void> {
|
export async function registerServices(pluginDetails: PluginDetails, container: MedusaContainer): Promise<void> {
|
||||||
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {})
|
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*.js`, {})
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map(async (fn) => {
|
files.map(async (fn) => {
|
||||||
const loaded = require(fn).default
|
const loaded = require(fn).default
|
||||||
|
|||||||
@@ -116,6 +116,24 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
.execute()
|
.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(
|
public async findManyForVariantInRegion(
|
||||||
variant_id: string,
|
variant_id: string,
|
||||||
region_id?: string,
|
region_id?: string,
|
||||||
@@ -126,21 +144,17 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
const date = new Date()
|
const date = new Date()
|
||||||
|
|
||||||
const qb = this.createQueryBuilder("ma")
|
const qb = this.createQueryBuilder("ma")
|
||||||
.leftJoinAndSelect(
|
.leftJoinAndSelect("ma.price_list", "price_list")
|
||||||
"ma.price_list",
|
|
||||||
"price_list",
|
|
||||||
"ma.price_list_id = price_list.id "
|
|
||||||
)
|
|
||||||
.where({ variant_id: variant_id })
|
.where({ variant_id: variant_id })
|
||||||
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
|
.andWhere("(ma.price_list_id is null or price_list.status = 'active')")
|
||||||
.andWhere(
|
.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(),
|
date: date.toUTCString(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.andWhere(
|
.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(),
|
date: date.toUTCString(),
|
||||||
}
|
}
|
||||||
@@ -155,23 +169,19 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (!customer_id && !include_discount_prices) {
|
} else if (!customer_id && !include_discount_prices) {
|
||||||
qb.andWhere("price_list IS null")
|
qb.andWhere("price_list.id IS null")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customer_id) {
|
if (customer_id) {
|
||||||
qb.leftJoin("price_list.customer_groups", "cgroup")
|
qb.leftJoin("price_list.customer_groups", "cgroup")
|
||||||
.leftJoin(
|
.leftJoin("customer_group_customers", "cgc", "cgc.customer_group_id = cgroup.id")
|
||||||
"customer_group_customers",
|
.andWhere("(cgc.customer_group_id is null OR cgc.customer_id = :customer_id)", {
|
||||||
"cgc",
|
|
||||||
"cgc.customer_group_id = cgroup.id"
|
|
||||||
)
|
|
||||||
.andWhere("(cgc is null OR cgc.customer_id = :customer_id)", {
|
|
||||||
customer_id,
|
customer_id,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
qb.leftJoin("price_list.customer_groups", "cgroup").andWhere(
|
qb
|
||||||
"cgroup.id is null"
|
.leftJoin("price_list.customer_groups", "cgroup")
|
||||||
)
|
.andWhere("cgroup.id is null")
|
||||||
}
|
}
|
||||||
return await qb.getManyAndCount()
|
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 { PriceList } from "../models/price-list"
|
||||||
|
import { CustomFindOptions } from "../types/common"
|
||||||
|
|
||||||
|
type PriceListFindOptions = CustomFindOptions<PriceList, 'status' | 'type'>
|
||||||
|
|
||||||
@EntityRepository(PriceList)
|
@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("LineItemService", () => {
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const lineItemRepository = MockRepository({})
|
const lineItemRepository = MockRepository({
|
||||||
|
create: (data) => data
|
||||||
|
})
|
||||||
|
|
||||||
const cartRepository = MockRepository({
|
const cartRepository = MockRepository({
|
||||||
findOne: () =>
|
findOne: () =>
|
||||||
@@ -105,9 +107,10 @@ describe("LineItemService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("successfully create a line item giftcard", async () => {
|
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-giftcard"),
|
||||||
IdMap.getId("test-region")
|
IdMap.getId("test-region"),
|
||||||
|
1
|
||||||
)
|
)
|
||||||
|
|
||||||
await lineItemService.create({
|
await lineItemService.create({
|
||||||
@@ -115,8 +118,8 @@ describe("LineItemService", () => {
|
|||||||
cart_id: IdMap.getId("test-cart"),
|
cart_id: IdMap.getId("test-cart"),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
|
expect(lineItemRepository.create).toHaveBeenCalledTimes(2)
|
||||||
expect(lineItemRepository.create).toHaveBeenCalledWith({
|
expect(lineItemRepository.create).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||||
allow_discounts: false,
|
allow_discounts: false,
|
||||||
variant_id: IdMap.getId("test-giftcard"),
|
variant_id: IdMap.getId("test-giftcard"),
|
||||||
cart_id: IdMap.getId("test-cart"),
|
cart_id: IdMap.getId("test-cart"),
|
||||||
@@ -128,7 +131,7 @@ describe("LineItemService", () => {
|
|||||||
is_giftcard: true,
|
is_giftcard: true,
|
||||||
should_merge: true,
|
should_merge: true,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
})
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import { MedusaError, Validator } from "medusa-core-utils"
|
import { MedusaError, Validator } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
|
||||||
import { DeepPartial, EntityManager, In } from "typeorm"
|
import { DeepPartial, EntityManager, In } from "typeorm"
|
||||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||||
import { Address } from "../models/address"
|
import { Address } from "../models/address"
|
||||||
@@ -37,7 +36,8 @@ import InventoryService from "./inventory"
|
|||||||
import CustomShippingOptionService from "./custom-shipping-option"
|
import CustomShippingOptionService from "./custom-shipping-option"
|
||||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||||
import { LineItemRepository } from "../repositories/line-item"
|
import { LineItemRepository } from "../repositories/line-item"
|
||||||
import { raw } from "express"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
|
import { buildQuery, setMetadata, validateId } from "../utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -71,14 +71,16 @@ type TotalsConfig = {
|
|||||||
/* Provides layer to manipulate carts.
|
/* Provides layer to manipulate carts.
|
||||||
* @implements BaseService
|
* @implements BaseService
|
||||||
*/
|
*/
|
||||||
class CartService extends BaseService {
|
class CartService extends TransactionBaseService<CartService> {
|
||||||
static Events = {
|
static readonly Events = {
|
||||||
CUSTOMER_UPDATED: "cart.customer_updated",
|
CUSTOMER_UPDATED: "cart.customer_updated",
|
||||||
CREATED: "cart.created",
|
CREATED: "cart.created",
|
||||||
UPDATED: "cart.updated",
|
UPDATED: "cart.updated",
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly manager_: EntityManager
|
protected manager_: EntityManager
|
||||||
|
protected transactionManager_: EntityManager | undefined
|
||||||
|
|
||||||
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
|
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
|
||||||
protected readonly cartRepository_: typeof CartRepository
|
protected readonly cartRepository_: typeof CartRepository
|
||||||
protected readonly addressRepository_: typeof AddressRepository
|
protected readonly addressRepository_: typeof AddressRepository
|
||||||
@@ -125,7 +127,8 @@ class CartService extends BaseService {
|
|||||||
lineItemAdjustmentService,
|
lineItemAdjustmentService,
|
||||||
priceSelectionStrategy,
|
priceSelectionStrategy,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super()
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
super(arguments[0])
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.shippingMethodRepository_ = shippingMethodRepository
|
this.shippingMethodRepository_ = shippingMethodRepository
|
||||||
@@ -151,41 +154,6 @@ class CartService extends BaseService {
|
|||||||
this.priceSelectionStrategy_ = priceSelectionStrategy
|
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_(
|
protected transformQueryForTotals_(
|
||||||
config: FindConfig<Cart>
|
config: FindConfig<Cart>
|
||||||
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
|
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
|
||||||
@@ -293,7 +261,7 @@ class CartService extends BaseService {
|
|||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const query = this.buildQuery_(selector, config)
|
const query = buildQuery<Cart>(selector, config)
|
||||||
return await cartRepo.find(query)
|
return await cartRepo.find(query)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -316,12 +284,12 @@ class CartService extends BaseService {
|
|||||||
const cartRepo = transactionManager.getCustomRepository(
|
const cartRepo = transactionManager.getCustomRepository(
|
||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
const validatedId = this.validateId_(cartId)
|
const validatedId = validateId(cartId)
|
||||||
|
|
||||||
const { select, relations, totalsToSelect } =
|
const { select, relations, totalsToSelect } =
|
||||||
this.transformQueryForTotals_(options)
|
this.transformQueryForTotals_(options)
|
||||||
|
|
||||||
const query = this.buildQuery_(
|
const query = buildQuery<Cart>(
|
||||||
{ id: validatedId },
|
{ id: validatedId },
|
||||||
{ ...options, select, relations }
|
{ ...options, select, relations }
|
||||||
)
|
)
|
||||||
@@ -706,16 +674,20 @@ class CartService extends BaseService {
|
|||||||
* @param shouldAdd - flag to indicate, if we should add or remove
|
* @param shouldAdd - flag to indicate, if we should add or remove
|
||||||
* @return void
|
* @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) {
|
if (cart.shipping_methods?.length) {
|
||||||
const shippingMethodRepository =
|
const shippingMethodRepository = transactionManager.getCustomRepository(
|
||||||
this.transactionManager_.getCustomRepository(
|
this.shippingMethodRepository_
|
||||||
this.shippingMethodRepository_
|
)
|
||||||
)
|
|
||||||
|
|
||||||
// if any free shipping discounts, we ensure to update shipping method amount
|
// if any free shipping discounts, we ensure to update shipping method amount
|
||||||
if (shouldAdd) {
|
if (shouldAdd) {
|
||||||
return shippingMethodRepository.update(
|
await shippingMethodRepository.update(
|
||||||
{
|
{
|
||||||
id: In(
|
id: In(
|
||||||
cart.shipping_methods.map((shippingMethod) => shippingMethod.id)
|
cart.shipping_methods.map((shippingMethod) => shippingMethod.id)
|
||||||
@@ -847,8 +819,8 @@ class CartService extends BaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("metadata" in data) {
|
if (data?.metadata) {
|
||||||
cart.metadata = this.setMetadata_(cart, data.metadata)
|
cart.metadata = setMetadata(cart, data.metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("context" in data) {
|
if ("context" in data) {
|
||||||
@@ -1344,7 +1316,7 @@ class CartService extends BaseService {
|
|||||||
* @param cartOrCartId - the id of the cart to set payment session for
|
* @param cartOrCartId - the id of the cart to set payment session for
|
||||||
* @return the result of the update operation.
|
* @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_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const psRepo = transactionManager.getCustomRepository(
|
const psRepo = transactionManager.getCustomRepository(
|
||||||
@@ -1671,6 +1643,8 @@ class CartService extends BaseService {
|
|||||||
regionId?: string,
|
regionId?: string,
|
||||||
customer_id?: string
|
customer_id?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
// If the cart contains items, we update the price of the items
|
// If the cart contains items, we update the price of the items
|
||||||
// to match the updated region or customer id (keeping the old
|
// to match the updated region or customer id (keeping the old
|
||||||
// value if it exists)
|
// value if it exists)
|
||||||
@@ -1685,7 +1659,7 @@ class CartService extends BaseService {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
cart.items.map(async (item) => {
|
cart.items.map(async (item) => {
|
||||||
const availablePrice = await this.priceSelectionStrategy_
|
const availablePrice = await this.priceSelectionStrategy_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.calculateVariantPrice(item.variant_id, {
|
.calculateVariantPrice(item.variant_id, {
|
||||||
region_id: region.id,
|
region_id: region.id,
|
||||||
currency_code: region.currency_code,
|
currency_code: region.currency_code,
|
||||||
@@ -1700,14 +1674,14 @@ class CartService extends BaseService {
|
|||||||
availablePrice.calculatedPrice !== null
|
availablePrice.calculatedPrice !== null
|
||||||
) {
|
) {
|
||||||
return this.lineItemService_
|
return this.lineItemService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.update(item.id, {
|
.update(item.id, {
|
||||||
has_shipping: false,
|
has_shipping: false,
|
||||||
unit_price: availablePrice.calculatedPrice,
|
unit_price: availablePrice.calculatedPrice,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await this.lineItemService_
|
await this.lineItemService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.delete(item.id)
|
.delete(item.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1729,6 +1703,8 @@ class CartService extends BaseService {
|
|||||||
regionId: string,
|
regionId: string,
|
||||||
countryCode: string | null
|
countryCode: string | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
if (cart.completed_at || cart.payment_authorized_at) {
|
if (cart.completed_at || cart.payment_authorized_at) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
@@ -1742,7 +1718,7 @@ class CartService extends BaseService {
|
|||||||
.retrieve(regionId, {
|
.retrieve(regionId, {
|
||||||
relations: ["countries"],
|
relations: ["countries"],
|
||||||
})
|
})
|
||||||
const addrRepo = this.transactionManager_.getCustomRepository(
|
const addrRepo = transactionManager.getCustomRepository(
|
||||||
this.addressRepository_
|
this.addressRepository_
|
||||||
)
|
)
|
||||||
cart.region = region
|
cart.region = region
|
||||||
@@ -1820,7 +1796,7 @@ class CartService extends BaseService {
|
|||||||
// new shipping method
|
// new shipping method
|
||||||
if (cart.shipping_methods && cart.shipping_methods.length) {
|
if (cart.shipping_methods && cart.shipping_methods.length) {
|
||||||
await this.shippingOptionService_
|
await this.shippingOptionService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.deleteShippingMethods(cart.shipping_methods)
|
.deleteShippingMethods(cart.shipping_methods)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1833,7 +1809,7 @@ class CartService extends BaseService {
|
|||||||
cart.gift_cards = []
|
cart.gift_cards = []
|
||||||
|
|
||||||
if (cart.payment_sessions && cart.payment_sessions.length) {
|
if (cart.payment_sessions && cart.payment_sessions.length) {
|
||||||
const paymentSessionRepo = this.transactionManager_.getCustomRepository(
|
const paymentSessionRepo = transactionManager.getCustomRepository(
|
||||||
this.paymentSessionRepository_
|
this.paymentSessionRepository_
|
||||||
)
|
)
|
||||||
await paymentSessionRepo.delete({
|
await paymentSessionRepo.delete({
|
||||||
@@ -1851,7 +1827,7 @@ class CartService extends BaseService {
|
|||||||
* @param cartId - the id of the cart to delete
|
* @param cartId - the id of the cart to delete
|
||||||
* @return the deleted cart or undefined if the cart was not found.
|
* @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_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const cart = await this.retrieve(cartId, {
|
const cart = await this.retrieve(cartId, {
|
||||||
@@ -1905,7 +1881,7 @@ class CartService extends BaseService {
|
|||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const validatedId = this.validateId_(cartId)
|
const validatedId = validateId(cartId)
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
@@ -1964,20 +1940,22 @@ class CartService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async refreshAdjustments_(cart: Cart): Promise<void> {
|
protected async refreshAdjustments_(cart: Cart): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
const nonReturnLineIDs = cart.items
|
const nonReturnLineIDs = cart.items
|
||||||
.filter((item) => !item.is_return)
|
.filter((item) => !item.is_return)
|
||||||
.map((i) => i.id)
|
.map((i) => i.id)
|
||||||
|
|
||||||
// delete all old non return line item adjustments
|
// delete all old non return line item adjustments
|
||||||
await this.lineItemAdjustmentService_
|
await this.lineItemAdjustmentService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.delete({
|
.delete({
|
||||||
item_id: nonReturnLineIDs,
|
item_id: nonReturnLineIDs,
|
||||||
})
|
})
|
||||||
|
|
||||||
// potentially create/update line item adjustments
|
// potentially create/update line item adjustments
|
||||||
await this.lineItemAdjustmentService_
|
await this.lineItemAdjustmentService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(transactionManager)
|
||||||
.createAdjustments(cart)
|
.createAdjustments(cart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1993,7 +1971,7 @@ class CartService extends BaseService {
|
|||||||
const cartRepo = transactionManager.getCustomRepository(
|
const cartRepo = transactionManager.getCustomRepository(
|
||||||
this.cartRepository_
|
this.cartRepository_
|
||||||
)
|
)
|
||||||
const validatedId = this.validateId_(cartId)
|
const validatedId = validateId(cartId)
|
||||||
|
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
throw new MedusaError(
|
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,
|
should_merge: shouldMerge,
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineLitemRepo = transactionManager.getCustomRepository(
|
const lineItemRepo = transactionManager.getCustomRepository(
|
||||||
this.lineItemRepository_
|
this.lineItemRepository_
|
||||||
)
|
)
|
||||||
const lineItem = lineLitemRepo.create(rawLineItem)
|
const lineItem = lineItemRepo.create(rawLineItem)
|
||||||
|
|
||||||
if (context.cart) {
|
if (context.cart) {
|
||||||
const adjustments = await this.lineItemAdjustmentService_
|
const adjustments = await this.lineItemAdjustmentService_
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { MedusaError } from "medusa-core-utils"
|
|||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { CustomerGroupService } from "."
|
import { CustomerGroupService } from "."
|
||||||
|
import { Product } from "../models"
|
||||||
import { CustomerGroup } from "../models/customer-group"
|
import { CustomerGroup } from "../models/customer-group"
|
||||||
import { PriceList } from "../models/price-list"
|
import { PriceList } from "../models/price-list"
|
||||||
import { MoneyAmountRepository } from "../repositories/money-amount"
|
import { MoneyAmountRepository } from "../repositories/money-amount"
|
||||||
@@ -14,10 +15,12 @@ import {
|
|||||||
UpdatePriceListInput,
|
UpdatePriceListInput,
|
||||||
} from "../types/price-list"
|
} from "../types/price-list"
|
||||||
import { formatException } from "../utils/exception-formatter"
|
import { formatException } from "../utils/exception-formatter"
|
||||||
|
import ProductService from "./product"
|
||||||
|
|
||||||
type PriceListConstructorProps = {
|
type PriceListConstructorProps = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
customerGroupService: CustomerGroupService
|
customerGroupService: CustomerGroupService
|
||||||
|
productService: ProductService
|
||||||
priceListRepository: typeof PriceListRepository
|
priceListRepository: typeof PriceListRepository
|
||||||
moneyAmountRepository: typeof MoneyAmountRepository
|
moneyAmountRepository: typeof MoneyAmountRepository
|
||||||
}
|
}
|
||||||
@@ -29,18 +32,21 @@ type PriceListConstructorProps = {
|
|||||||
class PriceListService extends BaseService {
|
class PriceListService extends BaseService {
|
||||||
private manager_: EntityManager
|
private manager_: EntityManager
|
||||||
private customerGroupService_: CustomerGroupService
|
private customerGroupService_: CustomerGroupService
|
||||||
|
private productService_: ProductService
|
||||||
private priceListRepo_: typeof PriceListRepository
|
private priceListRepo_: typeof PriceListRepository
|
||||||
private moneyAmountRepo_: typeof MoneyAmountRepository
|
private moneyAmountRepo_: typeof MoneyAmountRepository
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manager,
|
manager,
|
||||||
customerGroupService,
|
customerGroupService,
|
||||||
|
productService,
|
||||||
priceListRepository,
|
priceListRepository,
|
||||||
moneyAmountRepository,
|
moneyAmountRepository,
|
||||||
}: PriceListConstructorProps) {
|
}: PriceListConstructorProps) {
|
||||||
super()
|
super()
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.customerGroupService_ = customerGroupService
|
this.customerGroupService_ = customerGroupService
|
||||||
|
this.productService_ = productService
|
||||||
this.priceListRepo_ = priceListRepository
|
this.priceListRepo_ = priceListRepository
|
||||||
this.moneyAmountRepo_ = moneyAmountRepository
|
this.moneyAmountRepo_ = moneyAmountRepository
|
||||||
}
|
}
|
||||||
@@ -53,6 +59,7 @@ class PriceListService extends BaseService {
|
|||||||
const cloned = new PriceListService({
|
const cloned = new PriceListService({
|
||||||
manager: transactionManager,
|
manager: transactionManager,
|
||||||
customerGroupService: this.customerGroupService_,
|
customerGroupService: this.customerGroupService_,
|
||||||
|
productService: this.productService_,
|
||||||
priceListRepository: this.priceListRepo_,
|
priceListRepository: this.priceListRepo_,
|
||||||
moneyAmountRepository: this.moneyAmountRepo_,
|
moneyAmountRepository: this.moneyAmountRepo_,
|
||||||
})
|
})
|
||||||
@@ -253,9 +260,18 @@ class PriceListService extends BaseService {
|
|||||||
config: FindConfig<PriceList> = { skip: 0, take: 20 }
|
config: FindConfig<PriceList> = { skip: 0, take: 20 }
|
||||||
): Promise<[PriceList[], number]> {
|
): Promise<[PriceList[], number]> {
|
||||||
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
|
const priceListRepo = this.manager_.getCustomRepository(this.priceListRepo_)
|
||||||
|
const q = selector.q
|
||||||
|
const { relations, ...query } = this.buildQuery_(selector, config)
|
||||||
|
|
||||||
const query = this.buildQuery_(selector, config)
|
if (q) {
|
||||||
return await priceListRepo.findAndCount(query)
|
delete query.where.q
|
||||||
|
return await priceListRepo.getFreeTextSearchResultsAndCount(
|
||||||
|
q,
|
||||||
|
query,
|
||||||
|
relations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return await priceListRepo.findAndCount({ ...query, relations })
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertCustomerGroups_(
|
async upsertCustomerGroups_(
|
||||||
@@ -276,6 +292,50 @@ class PriceListService extends BaseService {
|
|||||||
|
|
||||||
await priceListRepo.save(priceList)
|
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
|
export default PriceListService
|
||||||
@@ -751,7 +751,7 @@ class ProductVariantService extends BaseService {
|
|||||||
|
|
||||||
const variant = await variantRepo.findOne({
|
const variant = await variantRepo.findOne({
|
||||||
where: { id: variantId },
|
where: { id: variantId },
|
||||||
relations: ["prices"],
|
relations: ["prices", "options"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
|
|||||||
@@ -668,7 +668,7 @@ class ProductService extends BaseService {
|
|||||||
// Should not fail, if product does not exist, since delete is idempotent
|
// Should not fail, if product does not exist, since delete is idempotent
|
||||||
const product = await productRepo.findOne(
|
const product = await productRepo.findOne(
|
||||||
{ id: productId },
|
{ id: productId },
|
||||||
{ relations: ["variants"] }
|
{ relations: ["variants", "variants.prices", "variants.options"] }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
|
|||||||
@@ -71,5 +71,5 @@ export type CartUpdateProps = {
|
|||||||
discounts?: Discount[]
|
discounts?: Discount[]
|
||||||
customer_id?: string
|
customer_id?: string
|
||||||
context?: object
|
context?: object
|
||||||
metadata?: object
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,34 @@
|
|||||||
import { Transform, Type } from "class-transformer"
|
import { Transform, Type } from "class-transformer"
|
||||||
import { IsDate, IsNumber, IsOptional, IsString } from "class-validator"
|
import { IsDate, IsNumber, IsOptional, IsString } from "class-validator"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
FindManyOptions,
|
||||||
|
FindOperator,
|
||||||
|
OrderByCondition,
|
||||||
|
} from "typeorm"
|
||||||
import { transformDate } from "../utils/validators/date-transform"
|
import { transformDate } from "../utils/validators/date-transform"
|
||||||
|
|
||||||
export type PartialPick<T, K extends keyof T> = {
|
export type PartialPick<T, K extends keyof T> = {
|
||||||
[P in K]?: T[P]
|
[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 =
|
export type TotalField =
|
||||||
| "shipping_total"
|
| "shipping_total"
|
||||||
| "discount_total"
|
| "discount_total"
|
||||||
@@ -25,6 +47,17 @@ export interface FindConfig<Entity> {
|
|||||||
order?: Record<string, "ASC" | "DESC">
|
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 PaginatedResponse = { limit: number; offset: number; count: number }
|
||||||
|
|
||||||
export type DeleteResponse = {
|
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