feat: Remove sales channels from pub keys (#6876)
**What** - Add workflow + step for detaching sales channels from pub API keys - Tweak linking error message to be more helpful - Add `removeRemoteLink` step to delete API key workflow
This commit is contained in:
8
.changeset/hot-bobcats-cough.md
Normal file
8
.changeset/hot-bobcats-cough.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/link-modules": patch
|
||||
"@medusajs/modules-sdk": patch
|
||||
---
|
||||
|
||||
feat: Remove sales channels from pub keys
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiKeyType } from "@medusajs/utils"
|
||||
import { ApiKeyType, ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { createAdminUser } from "../../../helpers/create-admin-user"
|
||||
|
||||
@@ -268,6 +268,127 @@ medusaIntegrationTestRunner({
|
||||
"Sales channels with IDs phony do not exist"
|
||||
)
|
||||
})
|
||||
|
||||
it("should detach sales channels from a publishable API key", async () => {
|
||||
const salesChannelRes = await api.post(
|
||||
`/admin/sales-channels`,
|
||||
{
|
||||
name: "Test Sales Channel",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { sales_channel } = salesChannelRes.data
|
||||
|
||||
const apiKeyRes = await api.post(
|
||||
`/admin/api-keys`,
|
||||
{
|
||||
title: "Test publishable KEY",
|
||||
type: ApiKeyType.PUBLISHABLE,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { api_key } = apiKeyRes.data
|
||||
|
||||
const keyWithChannelsRes = await api.post(
|
||||
`/admin/api-keys/${api_key.id}/sales-channels/batch/add`,
|
||||
{
|
||||
sales_channel_ids: [sales_channel.id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { api_key: keyWithChannels } = keyWithChannelsRes.data
|
||||
|
||||
expect(keyWithChannelsRes.status).toEqual(200)
|
||||
expect(keyWithChannels.title).toEqual("Test publishable KEY")
|
||||
expect(keyWithChannels.sales_channels).toEqual([
|
||||
expect.objectContaining({
|
||||
id: sales_channel.id,
|
||||
name: "Test Sales Channel",
|
||||
}),
|
||||
])
|
||||
|
||||
const keyWithoutChannelsRes = await api.post(
|
||||
`/admin/api-keys/${api_key.id}/sales-channels/batch/remove`,
|
||||
{
|
||||
sales_channel_ids: [sales_channel.id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { api_key: keyWithoutChannels } = keyWithoutChannelsRes.data
|
||||
|
||||
expect(keyWithoutChannelsRes.status).toEqual(200)
|
||||
expect(keyWithoutChannels.title).toEqual("Test publishable KEY")
|
||||
expect(keyWithoutChannels.sales_channels).toEqual([])
|
||||
})
|
||||
|
||||
it("should detach sales channels from a publishable API key on delete", async () => {
|
||||
const salesChannelRes = await api.post(
|
||||
`/admin/sales-channels`,
|
||||
{
|
||||
name: "Test Sales Channel",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { sales_channel } = salesChannelRes.data
|
||||
|
||||
const apiKeyRes = await api.post(
|
||||
`/admin/api-keys`,
|
||||
{
|
||||
title: "Test publishable KEY",
|
||||
type: ApiKeyType.PUBLISHABLE,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { api_key } = apiKeyRes.data
|
||||
|
||||
const keyWithChannelsRes = await api.post(
|
||||
`/admin/api-keys/${api_key.id}/sales-channels/batch/add`,
|
||||
{
|
||||
sales_channel_ids: [sales_channel.id],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const { api_key: keyWithChannels } = keyWithChannelsRes.data
|
||||
|
||||
expect(keyWithChannelsRes.status).toEqual(200)
|
||||
expect(keyWithChannels.title).toEqual("Test publishable KEY")
|
||||
expect(keyWithChannels.sales_channels).toEqual([
|
||||
expect.objectContaining({
|
||||
id: sales_channel.id,
|
||||
name: "Test Sales Channel",
|
||||
}),
|
||||
])
|
||||
|
||||
await api.delete(`/admin/api-keys/${api_key.id}`, adminHeaders)
|
||||
|
||||
const deletedApiKeys = await api.get(
|
||||
`/admin/api-keys?id=${api_key.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(deletedApiKeys.data.api_keys).toHaveLength(0)
|
||||
|
||||
const remoteQuery = container.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
|
||||
// Not the prettiest, but an easy way to check if the link was removed
|
||||
const channels = await remoteQuery({
|
||||
publishable_api_key_sales_channels: {
|
||||
__args: { sales_channel_id: [sales_channel.id] },
|
||||
fields: ["id", "sales_channel_id", "publishable_key_id"],
|
||||
},
|
||||
})
|
||||
|
||||
expect(channels).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -16,6 +16,10 @@ export const associateApiKeysWithSalesChannelsStep = createStep(
|
||||
async (input: StepInput, { container }) => {
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
if (!input.links) {
|
||||
return
|
||||
}
|
||||
|
||||
const links = input.links
|
||||
.map((link) => {
|
||||
return link.sales_channel_ids.map((id) => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
links: {
|
||||
api_key_id: string
|
||||
sales_channel_ids: string[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export const detachApiKeysWithSalesChannelsStepId =
|
||||
"detach-sales-channels-with-api-keys"
|
||||
export const detachApiKeysWithSalesChannelsStep = createStep(
|
||||
detachApiKeysWithSalesChannelsStepId,
|
||||
async (input: StepInput, { container }) => {
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
const links = input.links
|
||||
.map((link) => {
|
||||
return link.sales_channel_ids.map((id) => {
|
||||
return {
|
||||
[Modules.API_KEY]: {
|
||||
publishable_key_id: link.api_key_id,
|
||||
},
|
||||
[Modules.SALES_CHANNEL]: {
|
||||
sales_channel_id: id,
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
|
||||
await remoteLink.dismiss(links)
|
||||
|
||||
return new StepResponse(void 0, links)
|
||||
},
|
||||
async (links, { container }) => {
|
||||
if (!links?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
|
||||
await remoteLink.create(links)
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./associate-sales-channels-with-publishable-keys"
|
||||
export * from "./create-api-keys"
|
||||
export * from "./delete-api-keys"
|
||||
export * from "./detach-sales-channels-from-publishable-keys"
|
||||
export * from "./revoke-api-keys"
|
||||
export * from "./update-api-keys"
|
||||
export * from "./validate-sales-channel-exists"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { removeRemoteLinkStep } from "../../common/steps/remove-remote-links"
|
||||
import { deleteApiKeysStep } from "../steps"
|
||||
|
||||
type WorkflowInput = { ids: string[] }
|
||||
@@ -7,6 +9,11 @@ export const deleteApiKeysWorkflowId = "delete-api-keys"
|
||||
export const deleteApiKeysWorkflow = createWorkflow(
|
||||
deleteApiKeysWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
|
||||
return deleteApiKeysStep(input.ids)
|
||||
deleteApiKeysStep(input.ids)
|
||||
|
||||
// Please note, the ids here should be publishable key IDs
|
||||
removeRemoteLinkStep({
|
||||
[Modules.API_KEY]: { publishable_key_id: input.ids },
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./delete-api-keys"
|
||||
export * from "./update-api-keys"
|
||||
export * from "./revoke-api-keys"
|
||||
export * from "./add-sales-channels-to-publishable-key"
|
||||
export * from "./remove-sales-channels-from-publishable-key"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { detachApiKeysWithSalesChannelsStep } from "../steps/detach-sales-channels-from-publishable-keys"
|
||||
|
||||
type WorkflowInput = {
|
||||
data: {
|
||||
api_key_id: string
|
||||
sales_channel_ids: string[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export const removeSalesChannelsFromApiKeyWorkflowId =
|
||||
"remove-sales-channels-from-api-key"
|
||||
export const removeSalesChannelsFromApiKeyWorkflow = createWorkflow(
|
||||
removeSalesChannelsFromApiKeyWorkflowId,
|
||||
(input: WorkflowData<WorkflowInput>) => {
|
||||
detachApiKeysWithSalesChannelsStep({ links: input.data })
|
||||
}
|
||||
)
|
||||
@@ -11,10 +11,10 @@ export const PublishableApiKeySalesChannel: ModuleJoinerConfig = {
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
name: "publishable_api_key_sales_channel",
|
||||
},
|
||||
{
|
||||
name: "publishable_api_key_sales_channels",
|
||||
name: [
|
||||
"publishable_api_key_sales_channel",
|
||||
"publishable_api_key_sales_channels",
|
||||
],
|
||||
},
|
||||
],
|
||||
primaryKeys: ["id", "publishable_key_id", "sales_channel_id"],
|
||||
@@ -49,7 +49,7 @@ export const PublishableApiKeySalesChannel: ModuleJoinerConfig = {
|
||||
{
|
||||
serviceName: Modules.SALES_CHANNEL,
|
||||
fieldAlias: {
|
||||
api_keys: "api_keys_link.api_key",
|
||||
publishable_api_keys: "api_keys_link.api_key",
|
||||
},
|
||||
relationship: {
|
||||
serviceName: LINKS.PublishableApiKeySalesChannel,
|
||||
|
||||
@@ -50,6 +50,18 @@ export const LINKS = {
|
||||
Modules.STOCK_LOCATION,
|
||||
"location_id"
|
||||
),
|
||||
PublishableApiKeySalesChannel: composeLinkName(
|
||||
Modules.API_KEY,
|
||||
"api_key_id",
|
||||
Modules.SALES_CHANNEL,
|
||||
"sales_channel_id"
|
||||
),
|
||||
ProductSalesChannel: composeLinkName(
|
||||
Modules.PRODUCT,
|
||||
"product_id",
|
||||
Modules.SALES_CHANNEL,
|
||||
"sales_channel_id"
|
||||
),
|
||||
|
||||
// Internal services
|
||||
ProductShippingProfile: composeLinkName(
|
||||
@@ -58,22 +70,10 @@ export const LINKS = {
|
||||
"shippingProfileService",
|
||||
"profile_id"
|
||||
),
|
||||
ProductSalesChannel: composeLinkName(
|
||||
Modules.PRODUCT,
|
||||
"product_id",
|
||||
Modules.SALES_CHANNEL,
|
||||
"sales_channel_id"
|
||||
),
|
||||
OrderSalesChannel: composeLinkName(
|
||||
"orderService",
|
||||
"order_id",
|
||||
Modules.SALES_CHANNEL,
|
||||
"sales_channel_id"
|
||||
),
|
||||
PublishableApiKeySalesChannel: composeLinkName(
|
||||
Modules.API_KEY,
|
||||
"api_key_id",
|
||||
Modules.SALES_CHANNEL,
|
||||
"sales_channel_id"
|
||||
),
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../../../../types/routing"
|
||||
import { AdminPostApiKeysApiKeySalesChannelsBatchReq } from "../../../../validators"
|
||||
import { AdminPostApiKeysApiKeySalesChannelsBatchAddReq } from "../../../../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const body = req.validatedBody as AdminPostApiKeysApiKeySalesChannelsBatchReq
|
||||
const body =
|
||||
req.validatedBody as AdminPostApiKeysApiKeySalesChannelsBatchAddReq
|
||||
|
||||
const apiKeyModule = req.scope.resolve(ModuleRegistrationName.API_KEY)
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { removeSalesChannelsFromApiKeyWorkflow } from "@medusajs/core-flows"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../../../../types/routing"
|
||||
import { AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq } from "../../../../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const body =
|
||||
req.validatedBody as AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq
|
||||
|
||||
const apiKeyModule = req.scope.resolve(ModuleRegistrationName.API_KEY)
|
||||
|
||||
const apiKey = await apiKeyModule.retrieve(req.params.id)
|
||||
|
||||
if (apiKey.type !== "publishable") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Sales channels can only be associated with publishable API keys"
|
||||
)
|
||||
}
|
||||
|
||||
const workflowInput = {
|
||||
data: [
|
||||
{
|
||||
api_key_id: req.params.id,
|
||||
sales_channel_ids: body.sales_channel_ids,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const { errors } = await removeSalesChannelsFromApiKeyWorkflow(req.scope).run(
|
||||
{
|
||||
input: workflowInput,
|
||||
throwOnError: false,
|
||||
}
|
||||
)
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "api_key",
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
variables: {
|
||||
id: req.params.id,
|
||||
},
|
||||
})
|
||||
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const [result] = await remoteQuery(query)
|
||||
|
||||
res.status(200).json({ api_key: result })
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
AdminGetApiKeysApiKeyParams,
|
||||
AdminGetApiKeysParams,
|
||||
AdminPostApiKeysApiKeyReq,
|
||||
AdminPostApiKeysApiKeySalesChannelsBatchReq,
|
||||
AdminPostApiKeysApiKeySalesChannelsBatchAddReq,
|
||||
AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq,
|
||||
AdminPostApiKeysReq,
|
||||
AdminRevokeApiKeysApiKeyReq,
|
||||
} from "./validators"
|
||||
@@ -84,7 +85,18 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
AdminGetApiKeysApiKeyParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
transformBody(AdminPostApiKeysApiKeySalesChannelsBatchReq),
|
||||
transformBody(AdminPostApiKeysApiKeySalesChannelsBatchAddReq),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/api-keys/:id/sales-channels/batch/remove",
|
||||
middlewares: [
|
||||
transformQuery(
|
||||
AdminGetApiKeysApiKeyParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
transformBody(AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -79,7 +79,10 @@ export class AdminRevokeApiKeysApiKeyReq {
|
||||
|
||||
export class AdminDeleteApiKeysApiKeyReq {}
|
||||
|
||||
export class AdminPostApiKeysApiKeySalesChannelsBatchReq {
|
||||
export class AdminPostApiKeysApiKeySalesChannelsBatchAddReq {
|
||||
@IsArray()
|
||||
sales_channel_ids: string[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export class AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq extends AdminPostApiKeysApiKeySalesChannelsBatchAddReq {}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
|
||||
import { isObject, promiseAll, toPascalCase } from "@medusajs/utils"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { linkingErrorMessage } from "./utils/linking-error"
|
||||
|
||||
export type DeleteEntityInput = {
|
||||
[moduleName: string]: { [linkableKey: string]: string | string[] }
|
||||
@@ -353,7 +354,13 @@ export class RemoteLink {
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
`Module to link ${moduleA}[${moduleAKey}] and ${moduleB}[${moduleBKey}] was not found.`
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "link",
|
||||
})
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
@@ -404,7 +411,13 @@ export class RemoteLink {
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
`Module to dismiss link ${moduleA}[${moduleAKey}] and ${moduleB}[${moduleBKey}] was not found.`
|
||||
linkingErrorMessage({
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey,
|
||||
type: "dismiss",
|
||||
})
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
|
||||
24
packages/modules-sdk/src/utils/linking-error.ts
Normal file
24
packages/modules-sdk/src/utils/linking-error.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
const typeToMethod = new Map([
|
||||
[`dismiss`, `dismiss`],
|
||||
[`link`, `create`],
|
||||
])
|
||||
|
||||
type LinkingErrorMessageInput = {
|
||||
moduleA: string
|
||||
moduleAKey: string
|
||||
moduleB: string
|
||||
moduleBKey: string
|
||||
type: "dismiss" | "link"
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Example: Module to dismiss salesChannel and apiKey by keys sales_channel_id and api_key_id was not found. Ensure the link exists, keys are correct, and the link is passed in the correct order to method 'remoteLink.dismiss'
|
||||
*/
|
||||
export const linkingErrorMessage = (input: LinkingErrorMessageInput) => {
|
||||
const { moduleA, moduleB, moduleAKey, moduleBKey, type } = input
|
||||
return `Module to type ${moduleA} and ${moduleB} by keys ${moduleAKey} and ${moduleBKey} was not found. Ensure the link exists, keys are correct, and link is passed in the correct order to method 'remoteLink.${typeToMethod.get(
|
||||
type
|
||||
)}'.
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user