feat: Sales Channels API routes + workflows (#6722)

**What**
- Admin sales channels API routes
- Workflows for creating, updating, and deleting sales channels
- Align `ISalesChannelModuleService` interface with update + upsert conventions
- Integrate v2 tests into v1 sales channels integration test suite
- Add metadata to sales channel entity

Sales channel <> Product management will come in a follow-up PR.

On the integration tests, creating, updating, and deleting are passing with the V2 flag enabled. You'll have to take my word for it (or run them yourself), as they are not included in the CI.
This commit is contained in:
Oli Juhl
2024-03-18 20:57:29 +00:00
committed by GitHub
parent 4b06c17dc0
commit 0219a8677b
28 changed files with 1640 additions and 1146 deletions
@@ -0,0 +1,85 @@
import {
deleteSalesChannelsWorkflow,
updateSalesChannelsWorkflow,
} from "@medusajs/core-flows"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import { defaultAdminSalesChannelFields } from "../query-config"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const variables = {
id: req.params.id,
}
const queryObject = remoteQueryObjectFromString({
entryPoint: "sales_channels",
variables,
fields: defaultAdminSalesChannelFields,
})
const [sales_channel] = await remoteQuery(queryObject)
res.json({ sales_channel })
}
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const { errors } = await updateSalesChannelsWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "sales_channels",
variables: { id: req.params.id },
fields: defaultAdminSalesChannelFields,
})
const [sales_channel] = await remoteQuery(queryObject)
res.status(200).json({ sales_channel })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const id = req.params.id
const { errors } = await deleteSalesChannelsWorkflow(req.scope).run({
input: { ids: [id] },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
id,
object: "sales-channel",
deleted: true,
})
}
@@ -0,0 +1,59 @@
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import * as QueryConfig from "./query-config"
import {
AdminGetSalesChannelsParams,
AdminGetSalesChannelsSalesChannelParams,
AdminPostSalesChannelsReq,
AdminPostSalesChannelsSalesChannelReq,
} from "./validators"
export const adminSalesChannelRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["ALL"],
matcher: "/admin/sales-channels*",
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
},
{
method: ["GET"],
matcher: "/admin/sales-channels",
middlewares: [
transformQuery(
AdminGetSalesChannelsParams,
QueryConfig.listTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/sales-channels/:id",
middlewares: [
transformQuery(
AdminGetSalesChannelsSalesChannelParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/sales-channels",
middlewares: [
transformBody(AdminPostSalesChannelsReq),
transformQuery(
AdminGetSalesChannelsSalesChannelParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/sales-channels/:id",
middlewares: [transformBody(AdminPostSalesChannelsSalesChannelReq)],
},
{
method: ["DELETE"],
matcher: "/admin/sales-channels/:id",
middlewares: [],
},
]
@@ -0,0 +1,19 @@
export const defaultAdminSalesChannelFields = [
"id",
"name",
"description",
"is_disabled",
"created_at",
"updated_at",
"deleted_at",
]
export const retrieveTransformQueryConfig = {
defaults: defaultAdminSalesChannelFields,
isList: false,
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
isList: true,
}
@@ -0,0 +1,65 @@
import { createSalesChannelsWorkflow } from "@medusajs/core-flows"
import { CreateSalesChannelDTO } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const variables = {
filters: req.filterableFields,
...req.remoteQueryConfig.pagination,
}
const queryObject = remoteQueryObjectFromString({
entryPoint: "sales_channels",
variables,
fields: req.remoteQueryConfig.fields,
})
const { rows: sales_channels, metadata } = await remoteQuery(queryObject)
res.json({
sales_channels,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<CreateSalesChannelDTO>,
res: MedusaResponse
) => {
const salesChannelsData = [req.validatedBody]
const { errors } = await createSalesChannelsWorkflow(req.scope).run({
input: { salesChannelsData },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "sales_channels",
variables: { id: req.params.id },
fields: req.remoteQueryConfig.fields,
})
const [sales_channel] = await remoteQuery(queryObject)
res.status(200).json({ sales_channel })
}
@@ -0,0 +1,102 @@
import { OperatorMap } from "@medusajs/types"
import { Type } from "class-transformer"
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
export class AdminGetSalesChannelsSalesChannelParams extends FindParams {}
export class AdminGetSalesChannelsParams extends extendedFindParamsMixin({
limit: 20,
offset: 0,
}) {
/**
* ID to filter sales channels by.
*/
@IsString()
@IsOptional()
id?: string
/**
* Name to filter sales channels by.
*/
@IsOptional()
@IsString()
name?: string
/**
* Description to filter sales channels by.
*/
@IsOptional()
@IsString()
description?: string
/**
* Date filters to apply on sales channels' `created_at` field.
*/
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
/**
* Date filters to apply on sales channels' `updated_at` field.
*/
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetSalesChannelsParams)
$and?: AdminGetSalesChannelsParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetSalesChannelsParams)
$or?: AdminGetSalesChannelsParams[]
}
export class AdminPostSalesChannelsReq {
@IsString()
name: string
@IsString()
@IsOptional()
description: string
@IsBoolean()
@IsOptional()
is_disabled?: boolean
@IsNotEmpty()
@IsString()
@IsOptional()
metadata?: Record<string, unknown>
}
export class AdminPostSalesChannelsSalesChannelReq {
@IsOptional()
@IsString()
name?: string
@IsOptional()
@IsString()
description?: string
@IsBoolean()
@IsOptional()
is_disabled?: boolean
@IsNotEmpty()
@IsString()
@IsOptional()
metadata?: Record<string, unknown>
}