feat(core-flows,modules-sdk,types,medusa,link-modules): adds variant <> inventory item link endpoints (#7576)
what: - adds variant inventory link management endpoints: ``` Link inventory item to variant POST /products/:id/variants/:vid/inventory-items Update variant's inventory item link POST /products/:id/variants/:vid/inventory-items/:iid Unlink variant's inventory item DELETE /products/:id/variants/:vid/inventory-items/:iid ``` - a batch endpoint that does the above 3 across variants ``` POST /products/:id/variants/inventory-items ```
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import {
|
|
||||||
createAdminUser,
|
|
||||||
adminHeaders,
|
|
||||||
} from "../../../../helpers/create-admin-user"
|
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -2870,6 +2870,298 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items", () => {
|
||||||
|
it("should throw an error when required attributes are not passed", async () => {
|
||||||
|
const { response } = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(400)
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
type: "invalid_data",
|
||||||
|
message:
|
||||||
|
"Invalid request: Field 'required_quantity' is required; Field 'inventory_item_id' is required",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully adds inventory item to a variant", async () => {
|
||||||
|
const inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
const res = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.variant.inventory_items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 5,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
||||||
|
let inventoryItem
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when required attributes are not passed", async () => {
|
||||||
|
const { response } = await api
|
||||||
|
.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(400)
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
type: "invalid_data",
|
||||||
|
message: "Invalid request: Field 'required_quantity' is required",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates an inventory item link to a variant", async () => {
|
||||||
|
const res = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
{ required_quantity: 10 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.variant.inventory_items)
|
||||||
|
expect(res.data.variant.inventory_items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 10,
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /admin/products/:id/variants/:variant_id/inventory-items/:inventory_id", () => {
|
||||||
|
let inventoryItem
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully deletes an inventory item link from a variant", async () => {
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items`,
|
||||||
|
{ inventory_item_id: inventoryItem.id, required_quantity: 5 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await api.delete(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${baseProduct.variants[0].id}/inventory-items/${inventoryItem.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
expect(res.data.parent.inventory_items)
|
||||||
|
expect(res.data.parent.inventory_items).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/products/:id/variants/:variant_id/inventory-items/batch", () => {
|
||||||
|
let inventoryItemToUpdate
|
||||||
|
let inventoryItemToDelete
|
||||||
|
let inventoryItemToCreate
|
||||||
|
let inventoryProduct
|
||||||
|
let inventoryVariant1
|
||||||
|
let inventoryVariant2
|
||||||
|
let inventoryVariant3
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
inventoryProduct = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: "product 1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "variant 1",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "variant 2",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "variant 3",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
inventoryVariant1 = inventoryProduct.variants[0]
|
||||||
|
inventoryVariant2 = inventoryProduct.variants[1]
|
||||||
|
inventoryVariant3 = inventoryProduct.variants[2]
|
||||||
|
|
||||||
|
inventoryItemToCreate = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "to-create" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
inventoryItemToUpdate = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "to-update" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
inventoryItemToDelete = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "to-delete" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemToUpdate.id,
|
||||||
|
required_quantity: 5,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemToDelete.id,
|
||||||
|
required_quantity: 10,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully creates, updates and deletes an inventory item link from a variant", async () => {
|
||||||
|
const res = await api.post(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/inventory-items/batch`,
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
required_quantity: 15,
|
||||||
|
inventory_item_id: inventoryItemToCreate.id,
|
||||||
|
variant_id: inventoryVariant3.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
update: [
|
||||||
|
{
|
||||||
|
required_quantity: 25,
|
||||||
|
inventory_item_id: inventoryItemToUpdate.id,
|
||||||
|
variant_id: inventoryVariant1.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
delete: [
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItemToDelete.id,
|
||||||
|
variant_id: inventoryVariant2.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
|
||||||
|
const createdLinkVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant3.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(createdLinkVariant.inventory_items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 15,
|
||||||
|
inventory_item_id: inventoryItemToCreate.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedLinkVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant1.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(updatedLinkVariant.inventory_items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: 25,
|
||||||
|
inventory_item_id: inventoryItemToUpdate.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const deletedLinkVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${baseProduct.id}/variants/${inventoryVariant2.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(deletedLinkVariant.inventory_items).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import {
|
||||||
|
createLinksWorkflow,
|
||||||
|
createLinksWorkflowId,
|
||||||
|
dismissLinksWorkflow,
|
||||||
|
dismissLinksWorkflowId,
|
||||||
|
updateLinksWorkflow,
|
||||||
|
updateLinksWorkflowId,
|
||||||
|
} from "@medusajs/core-flows"
|
||||||
|
import { Modules } from "@medusajs/utils"
|
||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
env: {},
|
||||||
|
testSuite: ({ getContainer, api, dbConnection }) => {
|
||||||
|
describe("Workflows: Common", () => {
|
||||||
|
let appContainer
|
||||||
|
let product
|
||||||
|
let variant
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
|
|
||||||
|
product = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{
|
||||||
|
title: "product 1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "variant 1",
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
variant = product.variants[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createLinksWorkflow", () => {
|
||||||
|
describe("compensation", () => {
|
||||||
|
it("should dismiss links when step throws an error", async () => {
|
||||||
|
const workflow = createLinksWorkflow(appContainer)
|
||||||
|
const workflowId = createLinksWorkflowId
|
||||||
|
const inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
workflow.appendAction("throw", workflowId, {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error(`Fail`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItem.id },
|
||||||
|
data: { required_quantity: 10 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "throw",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: expect.objectContaining({
|
||||||
|
message: `Fail`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const updatedVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(updatedVariant.inventory_items).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateLinksWorkflow", () => {
|
||||||
|
describe("compensation", () => {
|
||||||
|
it("should revert link data when step throws an error", async () => {
|
||||||
|
const workflow = updateLinksWorkflow(appContainer)
|
||||||
|
const workflowId = updateLinksWorkflowId
|
||||||
|
const originalQuantity = 5
|
||||||
|
const newQuantity = 10
|
||||||
|
const inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${product.id}/variants/${variant.id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: originalQuantity,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.appendAction("throw", workflowId, {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error(`Fail`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItem.id },
|
||||||
|
data: { required_quantity: newQuantity },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "throw",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: expect.objectContaining({
|
||||||
|
message: `Fail`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const updatedVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(updatedVariant.inventory_items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: originalQuantity,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when a link is not found", async () => {
|
||||||
|
const workflow = updateLinksWorkflow(appContainer)
|
||||||
|
const workflowId = updateLinksWorkflowId
|
||||||
|
|
||||||
|
workflow.appendAction("throw", workflowId, {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error(`Fail`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||||
|
[Modules.INVENTORY]: {
|
||||||
|
inventory_item_id: "does-not-exist-id",
|
||||||
|
},
|
||||||
|
data: { required_quantity: 10 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "update-remote-links-step",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: expect.objectContaining({
|
||||||
|
message: `Could not find all existing links from data`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("dismissLinksWorkflow", () => {
|
||||||
|
describe("compensation", () => {
|
||||||
|
it("should recreate dismissed links when step throws an error", async () => {
|
||||||
|
const originalQuantity = 10
|
||||||
|
const workflow = dismissLinksWorkflow(appContainer)
|
||||||
|
const workflowId = dismissLinksWorkflowId
|
||||||
|
const inventoryItem = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/inventory-items`,
|
||||||
|
{ sku: "12345" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.inventory_item
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/products/${product.id}/variants/${variant.id}/inventory-items`,
|
||||||
|
{
|
||||||
|
inventory_item_id: inventoryItem.id,
|
||||||
|
required_quantity: originalQuantity,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.appendAction("throw", workflowId, {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error(`Fail`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItem.id },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "throw",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: expect.objectContaining({
|
||||||
|
message: `Fail`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const updatedVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(updatedVariant.inventory_items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
required_quantity: originalQuantity,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should pass dismiss step if link not found if next step throws error", async () => {
|
||||||
|
const workflow = dismissLinksWorkflow(appContainer)
|
||||||
|
const workflowId = dismissLinksWorkflowId
|
||||||
|
|
||||||
|
workflow.appendAction("throw", workflowId, {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error(`Fail`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variant.id },
|
||||||
|
[Modules.INVENTORY]: {
|
||||||
|
inventory_item_id: "does-not-exist-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "throw",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: expect.objectContaining({
|
||||||
|
message: `Fail`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const updatedVariant = (
|
||||||
|
await api.get(
|
||||||
|
`/admin/products/${product.id}/variants/${variant.id}?fields=inventory_items.inventory.*,inventory_items.*`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.variant
|
||||||
|
|
||||||
|
expect(updatedVariant.inventory_items).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
export * from "./steps/remove-remote-links"
|
|
||||||
export * from "./steps/use-remote-query"
|
|
||||||
export * from "./steps/create-remote-links"
|
export * from "./steps/create-remote-links"
|
||||||
export * from "./steps/dismiss-remote-links"
|
export * from "./steps/dismiss-remote-links"
|
||||||
|
export * from "./steps/remove-remote-links"
|
||||||
|
export * from "./steps/use-remote-query"
|
||||||
|
export * from "./workflows/batch-links"
|
||||||
|
export * from "./workflows/create-links"
|
||||||
|
export * from "./workflows/dismiss-links"
|
||||||
|
export * from "./workflows/update-links"
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
||||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
|
||||||
|
|
||||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||||
type CreateRemoteLinksStepInput = LinkDefinition[]
|
|
||||||
|
|
||||||
export const createLinksStepId = "create-remote-links"
|
export const createLinksStepId = "create-remote-links"
|
||||||
export const createRemoteLinkStep = createStep(
|
export const createRemoteLinkStep = createStep(
|
||||||
createLinksStepId,
|
createLinksStepId,
|
||||||
async (data: CreateRemoteLinksStepInput, { container }) => {
|
async (data: LinkDefinition[], { container }) => {
|
||||||
const link = container.resolve<RemoteLink>(
|
const link = container.resolve<RemoteLink>(
|
||||||
ContainerRegistrationKeys.REMOTE_LINK
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ContainerRegistrationKeys } from "@medusajs/utils"
|
|||||||
|
|
||||||
type DismissRemoteLinksStepInput = LinkDefinition | LinkDefinition[]
|
type DismissRemoteLinksStepInput = LinkDefinition | LinkDefinition[]
|
||||||
|
|
||||||
|
// TODO: add ability for this step to restore links from only foreign keys
|
||||||
export const dismissRemoteLinkStepId = "dismiss-remote-links"
|
export const dismissRemoteLinkStepId = "dismiss-remote-links"
|
||||||
export const dismissRemoteLinkStep = createStep(
|
export const dismissRemoteLinkStep = createStep(
|
||||||
dismissRemoteLinkStepId,
|
dismissRemoteLinkStepId,
|
||||||
@@ -18,18 +19,27 @@ export const dismissRemoteLinkStep = createStep(
|
|||||||
const link = container.resolve<RemoteLink>(
|
const link = container.resolve<RemoteLink>(
|
||||||
ContainerRegistrationKeys.REMOTE_LINK
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Our current revert strategy for dismissed links are to recreate it again.
|
||||||
|
// This works when its just the primary keys, but when you have additional data
|
||||||
|
// in the links, we need to preserve them in order to recreate the links accurately.
|
||||||
|
const dataBeforeDismiss = (await link.list(data, {
|
||||||
|
asLinkDefinition: true,
|
||||||
|
})) as LinkDefinition[]
|
||||||
|
|
||||||
await link.dismiss(entries)
|
await link.dismiss(entries)
|
||||||
|
|
||||||
return new StepResponse(entries, entries)
|
return new StepResponse(entries, dataBeforeDismiss)
|
||||||
},
|
},
|
||||||
async (dismissdLinks, { container }) => {
|
async (dataBeforeDismiss, { container }) => {
|
||||||
if (!dismissdLinks) {
|
if (!dataBeforeDismiss?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = container.resolve<RemoteLink>(
|
const link = container.resolve<RemoteLink>(
|
||||||
ContainerRegistrationKeys.REMOTE_LINK
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
)
|
)
|
||||||
await link.create(dismissdLinks)
|
|
||||||
|
await link.create(dataBeforeDismiss)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { LinkDefinition, RemoteLink } from "@medusajs/modules-sdk"
|
||||||
|
import { ContainerRegistrationKeys, MedusaError } from "@medusajs/utils"
|
||||||
|
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export const updateRemoteLinksStepId = "update-remote-links-step"
|
||||||
|
export const updateRemoteLinksStep = createStep(
|
||||||
|
updateRemoteLinksStepId,
|
||||||
|
async (data: LinkDefinition[], { container }) => {
|
||||||
|
if (!data.length) {
|
||||||
|
return new StepResponse([], [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = container.resolve<RemoteLink>(
|
||||||
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch all existing links and throw an error if any weren't found
|
||||||
|
const dataBeforeUpdate = (await link.list(data, {
|
||||||
|
asLinkDefinition: true,
|
||||||
|
})) as LinkDefinition[]
|
||||||
|
|
||||||
|
const unequal = dataBeforeUpdate.length !== data.length
|
||||||
|
|
||||||
|
if (unequal) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Could not find all existing links from data`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// link.create here performs an upsert. By performing validation above, we can ensure
|
||||||
|
// that this method will always perform an update in these cases
|
||||||
|
await link.create(data)
|
||||||
|
|
||||||
|
return new StepResponse(data, dataBeforeUpdate)
|
||||||
|
},
|
||||||
|
async (dataBeforeUpdate, { container }) => {
|
||||||
|
if (!dataBeforeUpdate?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = container.resolve<RemoteLink>(
|
||||||
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
|
)
|
||||||
|
|
||||||
|
await link.create(dataBeforeUpdate)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||||
|
import { BatchWorkflowInput } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
parallelize,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { createRemoteLinkStep } from "../steps/create-remote-links"
|
||||||
|
import { dismissRemoteLinkStep } from "../steps/dismiss-remote-links"
|
||||||
|
import { updateRemoteLinksStep } from "../steps/update-remote-links"
|
||||||
|
|
||||||
|
export const batchLinksWorkflowId = "batch-links"
|
||||||
|
export const batchLinksWorkflow = createWorkflow(
|
||||||
|
batchLinksWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<
|
||||||
|
BatchWorkflowInput<LinkDefinition, LinkDefinition, LinkDefinition>
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const [created, updated, deleted] = parallelize(
|
||||||
|
createRemoteLinkStep(input.create || []),
|
||||||
|
updateRemoteLinksStep(input.update || []),
|
||||||
|
dismissRemoteLinkStep(input.delete || [])
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import { createRemoteLinkStep } from "../steps/create-remote-links"
|
||||||
|
|
||||||
|
export const createLinksWorkflowId = "create-link"
|
||||||
|
export const createLinksWorkflow = createWorkflow(
|
||||||
|
createLinksWorkflowId,
|
||||||
|
(input: WorkflowData<LinkDefinition[]>) => {
|
||||||
|
return createRemoteLinkStep(input)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import { dismissRemoteLinkStep } from "../steps/dismiss-remote-links"
|
||||||
|
|
||||||
|
export const dismissLinksWorkflowId = "dismiss-link"
|
||||||
|
export const dismissLinksWorkflow = createWorkflow(
|
||||||
|
dismissLinksWorkflowId,
|
||||||
|
(input: WorkflowData<LinkDefinition[]>) => {
|
||||||
|
return dismissRemoteLinkStep(input)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||||
|
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||||
|
import { updateRemoteLinksStep } from "../steps/update-remote-links"
|
||||||
|
|
||||||
|
export const updateLinksWorkflowId = "update-link"
|
||||||
|
export const updateLinksWorkflow = createWorkflow(
|
||||||
|
updateLinksWorkflowId,
|
||||||
|
(input: WorkflowData<LinkDefinition[]>) => {
|
||||||
|
return updateRemoteLinksStep(input)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { isObject, promiseAll, toPascalCase } from "@medusajs/utils"
|
import { isObject, promiseAll, toPascalCase } from "@medusajs/utils"
|
||||||
import { Modules } from "./definitions"
|
import { Modules } from "./definitions"
|
||||||
import { MedusaModule } from "./medusa-module"
|
import { MedusaModule } from "./medusa-module"
|
||||||
|
import { convertRecordsToLinkDefinition } from "./utils/convert-data-to-link-definition"
|
||||||
import { linkingErrorMessage } from "./utils/linking-error"
|
import { linkingErrorMessage } from "./utils/linking-error"
|
||||||
|
|
||||||
export type DeleteEntityInput = {
|
export type DeleteEntityInput = {
|
||||||
@@ -16,7 +17,8 @@ export type RestoreEntityInput = DeleteEntityInput
|
|||||||
|
|
||||||
export type LinkDefinition = {
|
export type LinkDefinition = {
|
||||||
[moduleName: string]: {
|
[moduleName: string]: {
|
||||||
[fieldName: string]: string
|
// TODO: changing this to any temporarily as the "data" attribute is not being picked up correctly
|
||||||
|
[fieldName: string]: any
|
||||||
}
|
}
|
||||||
} & {
|
} & {
|
||||||
data?: Record<string, unknown>
|
data?: Record<string, unknown>
|
||||||
@@ -41,6 +43,14 @@ type CascadeError = {
|
|||||||
error: Error
|
error: Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkDataConfig = {
|
||||||
|
moduleA: string
|
||||||
|
moduleB: string
|
||||||
|
primaryKeys: string[]
|
||||||
|
moduleAKey: string
|
||||||
|
moduleBKey: string
|
||||||
|
}
|
||||||
|
|
||||||
export class RemoteLink {
|
export class RemoteLink {
|
||||||
private modulesMap: Map<string, LoadedLinkModule> = new Map()
|
private modulesMap: Map<string, LoadedLinkModule> = new Map()
|
||||||
private relationsPairs: Map<string, LoadedLinkModule> = new Map()
|
private relationsPairs: Map<string, LoadedLinkModule> = new Map()
|
||||||
@@ -325,6 +335,48 @@ export class RemoteLink {
|
|||||||
return [errors.length ? errors : null, result]
|
return [errors.length ? errors : null, result]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getLinkModuleOrThrow(link: LinkDefinition): LoadedLinkModule {
|
||||||
|
const mods = Object.keys(link).filter((attr) => attr !== "data")
|
||||||
|
|
||||||
|
if (mods.length > 2) {
|
||||||
|
throw new Error(`Only two modules can be linked.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { moduleA, moduleB, moduleAKey, moduleBKey } =
|
||||||
|
this.getLinkDataConfig(link)
|
||||||
|
const service = this.getLinkModule(moduleA, moduleAKey, moduleB, moduleBKey)
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(
|
||||||
|
linkingErrorMessage({
|
||||||
|
moduleA,
|
||||||
|
moduleAKey,
|
||||||
|
moduleB,
|
||||||
|
moduleBKey,
|
||||||
|
type: "link",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLinkDataConfig(link: LinkDefinition): LinkDataConfig {
|
||||||
|
const moduleNames = Object.keys(link).filter((attr) => attr !== "data")
|
||||||
|
const [moduleA, moduleB] = moduleNames
|
||||||
|
const primaryKeys = Object.keys(link[moduleA])
|
||||||
|
const moduleAKey = primaryKeys.join(",")
|
||||||
|
const moduleBKey = Object.keys(link[moduleB]).join(",")
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleA,
|
||||||
|
moduleB,
|
||||||
|
primaryKeys,
|
||||||
|
moduleAKey,
|
||||||
|
moduleBKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
async create(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||||
const allLinks = Array.isArray(link) ? link : [link]
|
const allLinks = Array.isArray(link) ? link : [link]
|
||||||
const serviceLinks = new Map<
|
const serviceLinks = new Map<
|
||||||
@@ -332,114 +384,72 @@ export class RemoteLink {
|
|||||||
[string | string[], string, Record<string, unknown>?][]
|
[string | string[], string, Record<string, unknown>?][]
|
||||||
>()
|
>()
|
||||||
|
|
||||||
for (const rel of allLinks) {
|
for (const link of allLinks) {
|
||||||
const extraFields = rel.data
|
const service = this.getLinkModuleOrThrow(link)
|
||||||
delete rel.data
|
const { moduleA, moduleB, moduleBKey, primaryKeys } =
|
||||||
|
this.getLinkDataConfig(link)
|
||||||
|
|
||||||
const mods = Object.keys(rel)
|
if (!serviceLinks.has(service.__definition.key)) {
|
||||||
if (mods.length > 2) {
|
|
||||||
throw new Error(`Only two modules can be linked.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [moduleA, moduleB] = mods
|
|
||||||
const pk = Object.keys(rel[moduleA])
|
|
||||||
const moduleAKey = pk.join(",")
|
|
||||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
|
||||||
|
|
||||||
const service = this.getLinkModule(
|
|
||||||
moduleA,
|
|
||||||
moduleAKey,
|
|
||||||
moduleB,
|
|
||||||
moduleBKey
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!service) {
|
|
||||||
throw new Error(
|
|
||||||
linkingErrorMessage({
|
|
||||||
moduleA,
|
|
||||||
moduleAKey,
|
|
||||||
moduleB,
|
|
||||||
moduleBKey,
|
|
||||||
type: "link",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
|
||||||
serviceLinks.set(service.__definition.key, [])
|
serviceLinks.set(service.__definition.key, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkValue =
|
const pkValue =
|
||||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
primaryKeys.length === 1
|
||||||
|
? link[moduleA][primaryKeys[0]]
|
||||||
|
: primaryKeys.map((k) => link[moduleA][k])
|
||||||
|
|
||||||
const fields: unknown[] = [pkValue, rel[moduleB][moduleBKey]]
|
const fields: unknown[] = [pkValue, link[moduleB][moduleBKey]]
|
||||||
if (isObject(extraFields)) {
|
|
||||||
fields.push(extraFields)
|
if (isObject(link.data)) {
|
||||||
|
fields.push(link.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceLinks.get(service.__definition.key)?.push(fields as any)
|
serviceLinks.get(service.__definition.key)?.push(fields as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises: Promise<unknown[]>[] = []
|
const promises: Promise<unknown[]>[] = []
|
||||||
|
|
||||||
for (const [serviceName, links] of serviceLinks) {
|
for (const [serviceName, links] of serviceLinks) {
|
||||||
const service = this.modulesMap.get(serviceName)!
|
const service = this.modulesMap.get(serviceName)!
|
||||||
|
|
||||||
promises.push(service.create(links))
|
promises.push(service.create(links))
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await promiseAll(promises)
|
return (await promiseAll(promises)).flat()
|
||||||
return created.flat()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async dismiss(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
async dismiss(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||||
const allLinks = Array.isArray(link) ? link : [link]
|
const allLinks = Array.isArray(link) ? link : [link]
|
||||||
const serviceLinks = new Map<string, [string | string[], string][]>()
|
const serviceLinks = new Map<string, [string | string[], string][]>()
|
||||||
|
|
||||||
for (const rel of allLinks) {
|
for (const link of allLinks) {
|
||||||
const mods = Object.keys(rel)
|
const service = this.getLinkModuleOrThrow(link)
|
||||||
if (mods.length > 2) {
|
const { moduleA, moduleB, moduleBKey, primaryKeys } =
|
||||||
throw new Error(`Only two modules can be linked.`)
|
this.getLinkDataConfig(link)
|
||||||
}
|
|
||||||
|
|
||||||
const [moduleA, moduleB] = mods
|
if (!serviceLinks.has(service.__definition.key)) {
|
||||||
const pk = Object.keys(rel[moduleA])
|
|
||||||
const moduleAKey = pk.join(",")
|
|
||||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
|
||||||
|
|
||||||
const service = this.getLinkModule(
|
|
||||||
moduleA,
|
|
||||||
moduleAKey,
|
|
||||||
moduleB,
|
|
||||||
moduleBKey
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!service) {
|
|
||||||
throw new Error(
|
|
||||||
linkingErrorMessage({
|
|
||||||
moduleA,
|
|
||||||
moduleAKey,
|
|
||||||
moduleB,
|
|
||||||
moduleBKey,
|
|
||||||
type: "dismiss",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
|
||||||
serviceLinks.set(service.__definition.key, [])
|
serviceLinks.set(service.__definition.key, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkValue =
|
const pkValue =
|
||||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
primaryKeys.length === 1
|
||||||
|
? link[moduleA][primaryKeys[0]]
|
||||||
|
: primaryKeys.map((k) => link[moduleA][k])
|
||||||
|
|
||||||
serviceLinks
|
serviceLinks
|
||||||
.get(service.__definition.key)
|
.get(service.__definition.key)
|
||||||
?.push([pkValue, rel[moduleB][moduleBKey]])
|
?.push([pkValue, link[moduleB][moduleBKey]] as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises: Promise<unknown[]>[] = []
|
const promises: Promise<unknown[]>[] = []
|
||||||
|
|
||||||
for (const [serviceName, links] of serviceLinks) {
|
for (const [serviceName, links] of serviceLinks) {
|
||||||
const service = this.modulesMap.get(serviceName)!
|
const service = this.modulesMap.get(serviceName)!
|
||||||
|
|
||||||
promises.push(service.dismiss(links))
|
promises.push(service.dismiss(links))
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await promiseAll(promises)
|
return (await promiseAll(promises)).flat()
|
||||||
return created.flat()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(
|
async delete(
|
||||||
@@ -453,4 +463,45 @@ export class RemoteLink {
|
|||||||
): Promise<[CascadeError[] | null, RestoredIds]> {
|
): Promise<[CascadeError[] | null, RestoredIds]> {
|
||||||
return await this.executeCascade(removedServices, "restore")
|
return await this.executeCascade(removedServices, "restore")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async list(
|
||||||
|
link: LinkDefinition | LinkDefinition[],
|
||||||
|
options?: { asLinkDefinition?: boolean }
|
||||||
|
): Promise<(object | LinkDefinition)[]> {
|
||||||
|
const allLinks = Array.isArray(link) ? link : [link]
|
||||||
|
const serviceLinks = new Map<string, object[]>()
|
||||||
|
|
||||||
|
for (const link of allLinks) {
|
||||||
|
const service = this.getLinkModuleOrThrow(link)
|
||||||
|
const { moduleA, moduleB, moduleBKey, primaryKeys } =
|
||||||
|
this.getLinkDataConfig(link)
|
||||||
|
|
||||||
|
if (!serviceLinks.has(service.__definition.key)) {
|
||||||
|
serviceLinks.set(service.__definition.key, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceLinks.get(service.__definition.key)?.push({
|
||||||
|
...link[moduleA],
|
||||||
|
...link[moduleB],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Promise<object[]>[] = []
|
||||||
|
|
||||||
|
for (const [serviceName, filters] of serviceLinks) {
|
||||||
|
const service = this.modulesMap.get(serviceName)!
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
service
|
||||||
|
.list({ $or: filters })
|
||||||
|
.then((links: any[]) =>
|
||||||
|
options?.asLinkDefinition
|
||||||
|
? convertRecordsToLinkDefinition(links, service)
|
||||||
|
: links
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await promiseAll(promises)).flat()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { LoadedModule } from "@medusajs/types"
|
||||||
|
import { isPresent } from "@medusajs/utils"
|
||||||
|
import { LinkDefinition } from "../remote-link"
|
||||||
|
|
||||||
|
export const convertRecordsToLinkDefinition = (
|
||||||
|
links: object[],
|
||||||
|
service: LoadedModule
|
||||||
|
): LinkDefinition[] => {
|
||||||
|
const linkRelations = service.__joinerConfig.relationships || []
|
||||||
|
const linkDataFields = service.__joinerConfig.extraDataFields || []
|
||||||
|
|
||||||
|
const results: LinkDefinition[] = []
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
const result: LinkDefinition = {}
|
||||||
|
|
||||||
|
for (const relation of linkRelations) {
|
||||||
|
result[relation.serviceName] = {
|
||||||
|
[relation.foreignKey]: link[relation.foreignKey],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: LinkDefinition["data"] = {}
|
||||||
|
|
||||||
|
for (const dataField of linkDataFields) {
|
||||||
|
data[dataField] = link[dataField]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPresent(data)) {
|
||||||
|
result.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ export type LinkWorkflowInput = {
|
|||||||
remove?: string[]
|
remove?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BatchMethodRequest<TCreate, TUpdate> = {
|
export type BatchMethodRequest<TCreate, TUpdate, TDelete = string> = {
|
||||||
create?: TCreate[]
|
create?: TCreate[]
|
||||||
update?: TUpdate[]
|
update?: TUpdate[]
|
||||||
delete?: string[]
|
delete?: TDelete[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BatchMethodResponse<T> = {
|
export type BatchMethodResponse<T> = {
|
||||||
@@ -25,9 +25,10 @@ export type BatchMethodResponse<T> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BatchWorkflowInput<TCreate, TUpdate> = BatchMethodRequest<
|
export type BatchWorkflowInput<
|
||||||
TCreate,
|
TCreate,
|
||||||
TUpdate
|
TUpdate,
|
||||||
>
|
TDelete = string
|
||||||
|
> = BatchMethodRequest<TCreate, TUpdate, TDelete>
|
||||||
|
|
||||||
export type BatchWorkflowOutput<T> = BatchMethodResponse<T>
|
export type BatchWorkflowOutput<T> = BatchMethodResponse<T>
|
||||||
|
|||||||
@@ -128,6 +128,29 @@ export type ModulesResponse = {
|
|||||||
resolution: string | false
|
resolution: string | false
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
|
type ExtraFieldType =
|
||||||
|
| "date"
|
||||||
|
| "time"
|
||||||
|
| "datetime"
|
||||||
|
| "bigint"
|
||||||
|
| "blob"
|
||||||
|
| "uint8array"
|
||||||
|
| "array"
|
||||||
|
| "enumArray"
|
||||||
|
| "enum"
|
||||||
|
| "json"
|
||||||
|
| "integer"
|
||||||
|
| "smallint"
|
||||||
|
| "tinyint"
|
||||||
|
| "mediumint"
|
||||||
|
| "float"
|
||||||
|
| "double"
|
||||||
|
| "boolean"
|
||||||
|
| "decimal"
|
||||||
|
| "string"
|
||||||
|
| "uuid"
|
||||||
|
| "text"
|
||||||
|
|
||||||
export type ModuleJoinerConfig = Omit<
|
export type ModuleJoinerConfig = Omit<
|
||||||
JoinerServiceConfig,
|
JoinerServiceConfig,
|
||||||
"serviceName" | "primaryKeys" | "relationships" | "extends"
|
"serviceName" | "primaryKeys" | "relationships" | "extends"
|
||||||
@@ -164,6 +187,11 @@ export type ModuleJoinerConfig = Omit<
|
|||||||
* If true it expands a RemoteQuery property but doesn't create a pivot table
|
* If true it expands a RemoteQuery property but doesn't create a pivot table
|
||||||
*/
|
*/
|
||||||
isReadOnlyLink?: boolean
|
isReadOnlyLink?: boolean
|
||||||
|
/**
|
||||||
|
* Fields that will be part of the link record aside from the primary keys that can be updated
|
||||||
|
* If not explicitly defined, this array will be populated by databaseConfig.extraFields
|
||||||
|
*/
|
||||||
|
extraDataFields?: string[]
|
||||||
databaseConfig?: {
|
databaseConfig?: {
|
||||||
/**
|
/**
|
||||||
* Name of the pivot table. If not provided it is auto generated
|
* Name of the pivot table. If not provided it is auto generated
|
||||||
@@ -176,28 +204,7 @@ export type ModuleJoinerConfig = Omit<
|
|||||||
extraFields?: Record<
|
extraFields?: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
type:
|
type: ExtraFieldType
|
||||||
| "date"
|
|
||||||
| "time"
|
|
||||||
| "datetime"
|
|
||||||
| "bigint"
|
|
||||||
| "blob"
|
|
||||||
| "uint8array"
|
|
||||||
| "array"
|
|
||||||
| "enumArray"
|
|
||||||
| "enum"
|
|
||||||
| "json"
|
|
||||||
| "integer"
|
|
||||||
| "smallint"
|
|
||||||
| "tinyint"
|
|
||||||
| "mediumint"
|
|
||||||
| "float"
|
|
||||||
| "double"
|
|
||||||
| "boolean"
|
|
||||||
| "decimal"
|
|
||||||
| "string"
|
|
||||||
| "uuid"
|
|
||||||
| "text"
|
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
nullable?: boolean
|
nullable?: boolean
|
||||||
/**
|
/**
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
import { dismissLinksWorkflow, updateLinksWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { Modules } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../../../../types/routing"
|
||||||
|
import { refetchVariant } from "../../../../../helpers"
|
||||||
|
import { AdminUpdateVariantInventoryItemType } from "../../../../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminUpdateVariantInventoryItemType>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const variantId = req.params.variant_id
|
||||||
|
const inventoryItemId = req.params.inventory_item_id
|
||||||
|
|
||||||
|
await updateLinksWorkflow(req.scope).run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variantId },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItemId },
|
||||||
|
data: { required_quantity: req.validatedBody.required_quantity },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const variant = await refetchVariant(
|
||||||
|
variantId,
|
||||||
|
req.scope,
|
||||||
|
req.remoteQueryConfig.fields
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ variant })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE = async (
|
||||||
|
req: AuthenticatedMedusaRequest,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const variantId = req.params.variant_id
|
||||||
|
const inventoryItemId = req.params.inventory_item_id
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: [deleted],
|
||||||
|
} = await dismissLinksWorkflow(req.scope).run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variantId },
|
||||||
|
[Modules.INVENTORY]: { inventory_item_id: inventoryItemId },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const parent = await refetchVariant(
|
||||||
|
variantId,
|
||||||
|
req.scope,
|
||||||
|
req.remoteQueryConfig.fields
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
id: deleted,
|
||||||
|
object: "variant-inventory-item-link",
|
||||||
|
deleted: true,
|
||||||
|
parent,
|
||||||
|
})
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
import { createLinksWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { Modules } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../../../types/routing"
|
||||||
|
import { refetchVariant } from "../../../../helpers"
|
||||||
|
import { AdminCreateVariantInventoryItemType } from "../../../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminCreateVariantInventoryItemType>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const variantId = req.params.variant_id
|
||||||
|
|
||||||
|
await createLinksWorkflow(req.scope).run({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
[Modules.PRODUCT]: { variant_id: variantId },
|
||||||
|
[Modules.INVENTORY]: {
|
||||||
|
inventory_item_id: req.validatedBody.inventory_item_id,
|
||||||
|
},
|
||||||
|
data: { required_quantity: req.validatedBody.required_quantity },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const variant = await refetchVariant(
|
||||||
|
variantId,
|
||||||
|
req.scope,
|
||||||
|
req.remoteQueryConfig.fields
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ variant })
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { batchLinksWorkflow } from "@medusajs/core-flows"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../../../types/routing"
|
||||||
|
import { buildBatchVariantInventoryData } from "../../../../helpers"
|
||||||
|
import { AdminBatchVariantInventoryItemsType } from "../../../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminBatchVariantInventoryItemsType>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const { create = [], update = [], delete: toDelete = [] } = req.validatedBody
|
||||||
|
|
||||||
|
const { result } = await batchLinksWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
create: buildBatchVariantInventoryData(create),
|
||||||
|
update: buildBatchVariantInventoryData(update),
|
||||||
|
delete: buildBatchVariantInventoryData(toDelete),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
created: result.created,
|
||||||
|
updated: result.updated,
|
||||||
|
deleted: result.deleted,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LinkDefinition } from "@medusajs/modules-sdk"
|
||||||
import {
|
import {
|
||||||
BatchMethodResponse,
|
BatchMethodResponse,
|
||||||
MedusaContainer,
|
MedusaContainer,
|
||||||
@@ -5,10 +6,12 @@ import {
|
|||||||
ProductVariantDTO,
|
ProductVariantDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
Modules,
|
||||||
promiseAll,
|
promiseAll,
|
||||||
remoteQueryObjectFromString,
|
remoteQueryObjectFromString,
|
||||||
ContainerRegistrationKeys,
|
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
import { AdminBatchVariantInventoryItemsType } from "./validators"
|
||||||
|
|
||||||
const isPricing = (fieldName: string) =>
|
const isPricing = (fieldName: string) =>
|
||||||
fieldName.startsWith("variants.prices") ||
|
fieldName.startsWith("variants.prices") ||
|
||||||
@@ -89,6 +92,25 @@ export const refetchProduct = async (
|
|||||||
return products[0]
|
return products[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const refetchVariant = async (
|
||||||
|
variantId: string,
|
||||||
|
scope: MedusaContainer,
|
||||||
|
fields: string[]
|
||||||
|
) => {
|
||||||
|
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
const queryObject = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "product_variant",
|
||||||
|
variables: {
|
||||||
|
filters: { id: variantId },
|
||||||
|
},
|
||||||
|
fields: remapKeysForVariant(fields ?? []),
|
||||||
|
})
|
||||||
|
|
||||||
|
const [variant] = await remoteQuery(queryObject)
|
||||||
|
|
||||||
|
return remapVariantResponse(variant)
|
||||||
|
}
|
||||||
|
|
||||||
export const refetchBatchProducts = async (
|
export const refetchBatchProducts = async (
|
||||||
batchResult: BatchMethodResponse<ProductDTO>,
|
batchResult: BatchMethodResponse<ProductDTO>,
|
||||||
scope: MedusaContainer,
|
scope: MedusaContainer,
|
||||||
@@ -174,3 +196,31 @@ export const refetchBatchVariants = async (
|
|||||||
deleted: batchResult.deleted,
|
deleted: batchResult.deleted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const buildBatchVariantInventoryData = (
|
||||||
|
inputs:
|
||||||
|
| AdminBatchVariantInventoryItemsType["create"]
|
||||||
|
| AdminBatchVariantInventoryItemsType["update"]
|
||||||
|
| AdminBatchVariantInventoryItemsType["delete"]
|
||||||
|
) => {
|
||||||
|
const results: LinkDefinition[] = []
|
||||||
|
|
||||||
|
for (const input of inputs || []) {
|
||||||
|
const result: LinkDefinition = {
|
||||||
|
[Modules.PRODUCT]: { variant_id: input.variant_id },
|
||||||
|
[Modules.INVENTORY]: {
|
||||||
|
inventory_item_id: input.inventory_item_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("required_quantity" in input) {
|
||||||
|
result.data = {
|
||||||
|
required_quantity: input.required_quantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,20 +7,25 @@ import { createBatchBody } from "../../utils/validators"
|
|||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
import { maybeApplyPriceListsFilter } from "./utils"
|
import { maybeApplyPriceListsFilter } from "./utils"
|
||||||
import {
|
import {
|
||||||
AdminGetProductsParams,
|
AdminBatchCreateVariantInventoryItem,
|
||||||
|
AdminBatchDeleteVariantInventoryItem,
|
||||||
|
AdminBatchUpdateProduct,
|
||||||
|
AdminBatchUpdateProductVariant,
|
||||||
|
AdminBatchUpdateVariantInventoryItem,
|
||||||
AdminCreateProduct,
|
AdminCreateProduct,
|
||||||
AdminCreateProductOption,
|
AdminCreateProductOption,
|
||||||
AdminCreateProductVariant,
|
AdminCreateProductVariant,
|
||||||
|
AdminCreateVariantInventoryItem,
|
||||||
|
AdminGetProductOptionParams,
|
||||||
|
AdminGetProductOptionsParams,
|
||||||
|
AdminGetProductParams,
|
||||||
|
AdminGetProductsParams,
|
||||||
|
AdminGetProductVariantParams,
|
||||||
|
AdminGetProductVariantsParams,
|
||||||
AdminUpdateProduct,
|
AdminUpdateProduct,
|
||||||
AdminUpdateProductOption,
|
AdminUpdateProductOption,
|
||||||
AdminGetProductParams,
|
|
||||||
AdminGetProductVariantsParams,
|
|
||||||
AdminGetProductVariantParams,
|
|
||||||
AdminUpdateProductVariant,
|
AdminUpdateProductVariant,
|
||||||
AdminGetProductOptionsParams,
|
AdminUpdateVariantInventoryItem,
|
||||||
AdminGetProductOptionParams,
|
|
||||||
AdminBatchUpdateProduct,
|
|
||||||
AdminBatchUpdateProductVariant,
|
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
|
|
||||||
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
@@ -243,4 +248,57 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Variant inventory item endpoints
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/products/:id/variants/inventory-items/batch",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(
|
||||||
|
createBatchBody(
|
||||||
|
AdminBatchCreateVariantInventoryItem,
|
||||||
|
AdminBatchUpdateVariantInventoryItem,
|
||||||
|
AdminBatchDeleteVariantInventoryItem
|
||||||
|
)
|
||||||
|
),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetProductVariantParams,
|
||||||
|
QueryConfig.retrieveVariantConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/products/:id/variants/:variant_id/inventory-items",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminCreateVariantInventoryItem),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetProductVariantParams,
|
||||||
|
QueryConfig.retrieveVariantConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher:
|
||||||
|
"/admin/products/:id/variants/:variant_id/inventory-items/:inventory_item_id",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminUpdateVariantInventoryItem),
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetProductVariantParams,
|
||||||
|
QueryConfig.retrieveVariantConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["DELETE"],
|
||||||
|
matcher:
|
||||||
|
"/admin/products/:id/variants/:variant_id/inventory-items/:inventory_item_id",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetProductVariantParams,
|
||||||
|
QueryConfig.retrieveVariantConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BatchMethodRequest } from "@medusajs/types"
|
||||||
import { ProductStatus } from "@medusajs/utils"
|
import { ProductStatus } from "@medusajs/utils"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { GetProductsParams } from "../../utils/common-validators"
|
import { GetProductsParams } from "../../utils/common-validators"
|
||||||
@@ -235,3 +236,56 @@ export const AdminBatchUpdateProduct = AdminUpdateProduct.extend({
|
|||||||
// @ValidateNested({ each: true })
|
// @ValidateNested({ each: true })
|
||||||
// @IsArray()
|
// @IsArray()
|
||||||
// categories?: ProductProductCategoryReq[]
|
// categories?: ProductProductCategoryReq[]
|
||||||
|
|
||||||
|
export const AdminCreateVariantInventoryItem = z.object({
|
||||||
|
required_quantity: z.number(),
|
||||||
|
inventory_item_id: z.string(),
|
||||||
|
})
|
||||||
|
export type AdminCreateVariantInventoryItemType = z.infer<
|
||||||
|
typeof AdminCreateVariantInventoryItem
|
||||||
|
>
|
||||||
|
|
||||||
|
export const AdminUpdateVariantInventoryItem = z.object({
|
||||||
|
required_quantity: z.number(),
|
||||||
|
})
|
||||||
|
export type AdminUpdateVariantInventoryItemType = z.infer<
|
||||||
|
typeof AdminUpdateVariantInventoryItem
|
||||||
|
>
|
||||||
|
|
||||||
|
export const AdminBatchCreateVariantInventoryItem = z
|
||||||
|
.object({
|
||||||
|
required_quantity: z.number(),
|
||||||
|
inventory_item_id: z.string(),
|
||||||
|
variant_id: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
export type AdminBatchCreateVariantInventoryItemType = z.infer<
|
||||||
|
typeof AdminBatchCreateVariantInventoryItem
|
||||||
|
>
|
||||||
|
|
||||||
|
export const AdminBatchUpdateVariantInventoryItem = z
|
||||||
|
.object({
|
||||||
|
required_quantity: z.number(),
|
||||||
|
inventory_item_id: z.string(),
|
||||||
|
variant_id: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
export type AdminBatchUpdateVariantInventoryItemType = z.infer<
|
||||||
|
typeof AdminBatchUpdateVariantInventoryItem
|
||||||
|
>
|
||||||
|
|
||||||
|
export const AdminBatchDeleteVariantInventoryItem = z
|
||||||
|
.object({
|
||||||
|
inventory_item_id: z.string(),
|
||||||
|
variant_id: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
export type AdminBatchDeleteVariantInventoryItemType = z.infer<
|
||||||
|
typeof AdminBatchDeleteVariantInventoryItem
|
||||||
|
>
|
||||||
|
|
||||||
|
export type AdminBatchVariantInventoryItemsType = BatchMethodRequest<
|
||||||
|
AdminBatchCreateVariantInventoryItemType,
|
||||||
|
AdminBatchUpdateVariantInventoryItemType,
|
||||||
|
AdminBatchDeleteVariantInventoryItemType
|
||||||
|
>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { z } from "zod"
|
|||||||
|
|
||||||
export const createBatchBody = (
|
export const createBatchBody = (
|
||||||
createValidator: z.ZodType,
|
createValidator: z.ZodType,
|
||||||
updateValidator: z.ZodType
|
updateValidator: z.ZodType,
|
||||||
|
deleteValidator: z.ZodType = z.string()
|
||||||
) => {
|
) => {
|
||||||
return z.object({
|
return z.object({
|
||||||
create: z.array(createValidator).optional(),
|
create: z.array(createValidator).optional(),
|
||||||
update: z.array(updateValidator).optional(),
|
update: z.array(updateValidator).optional(),
|
||||||
delete: z.array(z.string()).optional(),
|
delete: z.array(deleteValidator).optional(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
|
MedusaModule,
|
||||||
MODULE_RESOURCE_TYPE,
|
MODULE_RESOURCE_TYPE,
|
||||||
MODULE_SCOPE,
|
MODULE_SCOPE,
|
||||||
MedusaModule,
|
|
||||||
ModuleRegistrationName,
|
ModuleRegistrationName,
|
||||||
} from "@medusajs/modules-sdk"
|
} from "@medusajs/modules-sdk"
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ModuleServiceInitializeOptions,
|
ModuleServiceInitializeOptions,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
arrayDifference,
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
lowerCaseFirst,
|
lowerCaseFirst,
|
||||||
simpleHash,
|
simpleHash,
|
||||||
@@ -50,7 +51,9 @@ export const initialize = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
for (const linkDefinition of allLinksToLoad) {
|
for (const linkDefinition of allLinksToLoad) {
|
||||||
const definition = JSON.parse(JSON.stringify(linkDefinition))
|
const definition: ModuleJoinerConfig = JSON.parse(
|
||||||
|
JSON.stringify(linkDefinition)
|
||||||
|
)
|
||||||
|
|
||||||
const [primary, foreign] = definition.relationships ?? []
|
const [primary, foreign] = definition.relationships ?? []
|
||||||
|
|
||||||
@@ -65,6 +68,24 @@ export const initialize = async (
|
|||||||
throw new Error(`Foreign key cannot be a composed key.`)
|
throw new Error(`Foreign key cannot be a composed key.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(definition.extraDataFields)) {
|
||||||
|
const extraDataFields = definition.extraDataFields
|
||||||
|
const definedDbFields = Object.keys(
|
||||||
|
definition.databaseConfig?.extraFields || {}
|
||||||
|
)
|
||||||
|
const difference = arrayDifference(extraDataFields, definedDbFields)
|
||||||
|
|
||||||
|
if (difference.length) {
|
||||||
|
throw new Error(
|
||||||
|
`extraDataFields (fieldNames: ${difference.join(
|
||||||
|
","
|
||||||
|
)}) need to be configured under databaseConfig (serviceName: ${
|
||||||
|
definition.serviceName
|
||||||
|
}).`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const serviceKey = !definition.isReadOnlyLink
|
const serviceKey = !definition.isReadOnlyLink
|
||||||
? lowerCaseFirst(
|
? lowerCaseFirst(
|
||||||
definition.serviceName ??
|
definition.serviceName ??
|
||||||
@@ -112,7 +133,10 @@ export const initialize = async (
|
|||||||
logger,
|
logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
definition.alias ??= []
|
if (!Array.isArray(definition.alias)) {
|
||||||
|
definition.alias = definition.alias ? [definition.alias] : []
|
||||||
|
}
|
||||||
|
|
||||||
for (const alias of definition.alias) {
|
for (const alias of definition.alias) {
|
||||||
alias.args ??= {}
|
alias.args ??= {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import { Constructor, ILinkModule, ModuleJoinerConfig } from "@medusajs/types"
|
import { Constructor, ILinkModule, ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
import { isDefined } from "@medusajs/utils"
|
||||||
import { LinkModuleService } from "@services"
|
import { LinkModuleService } from "@services"
|
||||||
|
|
||||||
export function getModuleService(
|
export function getModuleService(
|
||||||
joinerConfig: ModuleJoinerConfig
|
joinerConfig: ModuleJoinerConfig
|
||||||
): Constructor<ILinkModule> {
|
): Constructor<ILinkModule> {
|
||||||
const joinerConfig_ = JSON.parse(JSON.stringify(joinerConfig))
|
const joinerConfig_ = JSON.parse(JSON.stringify(joinerConfig))
|
||||||
|
const databaseConfig = joinerConfig_.databaseConfig
|
||||||
|
|
||||||
delete joinerConfig_.databaseConfig
|
delete joinerConfig_.databaseConfig
|
||||||
|
|
||||||
|
// If extraDataFields is not defined, pick the fields to populate and validate from the
|
||||||
|
// database config if any fields are provided.
|
||||||
|
if (!isDefined(joinerConfig_.extraDataFields)) {
|
||||||
|
joinerConfig_.extraDataFields = Object.keys(
|
||||||
|
databaseConfig.extraFields || {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return class LinkService extends LinkModuleService<unknown> {
|
return class LinkService extends LinkModuleService<unknown> {
|
||||||
override __joinerConfig(): ModuleJoinerConfig {
|
override __joinerConfig(): ModuleJoinerConfig {
|
||||||
return joinerConfig_ as ModuleJoinerConfig
|
return joinerConfig_ as ModuleJoinerConfig
|
||||||
|
|||||||
Reference in New Issue
Block a user