chore: Move most of the remaining endpoints to zod (#7096)

This commit is contained in:
Stevche Radevski
2024-04-18 14:12:13 +02:00
committed by GitHub
parent 62b9dcc6c1
commit be00a2eb51
31 changed files with 551 additions and 661 deletions

View File

@@ -1,25 +1,22 @@
import { revokeApiKeysWorkflow } from "@medusajs/core-flows"
import { RevokeApiKeyDTO } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { AdminRevokeApiKeyType } from "../../validators"
import { refetchApiKey } from "../../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest,
req: AuthenticatedMedusaRequest<AdminRevokeApiKeyType>,
res: MedusaResponse
) => {
const { errors } = await revokeApiKeysWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
revoke: {
...(req.validatedBody as Omit<RevokeApiKeyDTO, "revoked_by">),
...req.validatedBody,
revoked_by: req.auth.actor_id,
} as RevokeApiKeyDTO,
},
},
throwOnError: false,
})
@@ -28,17 +25,11 @@ export const POST = async (
throw errors[0].error
}
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "api_key",
variables: {
id: req.params.id,
},
fields: req.remoteQueryConfig.fields,
})
const [apiKey] = await remoteQuery(queryObject)
const apiKey = await refetchApiKey(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ api_key: apiKey })
}

View File

@@ -7,40 +7,34 @@ import {
MedusaResponse,
} from "../../../../types/routing"
import { UpdateApiKeyDTO } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { defaultAdminApiKeyFields } from "../query-config"
import { refetchApiKey } from "../helpers"
import { AdminUpdateApiKeyType } from "../validators"
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: "api_key",
variables,
fields: defaultAdminApiKeyFields,
})
const [apiKey] = await remoteQuery(queryObject)
const apiKey = await refetchApiKey(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ api_key: apiKey })
}
export const POST = async (
req: AuthenticatedMedusaRequest<Omit<UpdateApiKeyDTO, "id">>,
req: AuthenticatedMedusaRequest<AdminUpdateApiKeyType>,
res: MedusaResponse
) => {
const { result, errors } = await updateApiKeysWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody as UpdateApiKeyDTO,
update: req.validatedBody,
},
throwOnError: false,
})
@@ -49,17 +43,11 @@ export const POST = async (
throw errors[0].error
}
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "api_key",
variables: {
id: req.params.id,
},
fields: req.remoteQueryConfig.fields,
})
const [apiKey] = await remoteQuery(queryObject)
const apiKey = await refetchApiKey(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ api_key: apiKey })
}

View File

@@ -1,63 +0,0 @@
import { addSalesChannelsToApiKeyWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
ContainerRegistrationKeys,
MedusaError,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../../types/routing"
import { AdminPostApiKeysApiKeySalesChannelsBatchAddReq } from "../../../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const body =
req.validatedBody as AdminPostApiKeysApiKeySalesChannelsBatchAddReq
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 addSalesChannelsToApiKeyWorkflow(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 })
}

View File

@@ -1,65 +0,0 @@
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 })
}

View File

@@ -0,0 +1,83 @@
import {
addSalesChannelsToApiKeyWorkflow,
removeSalesChannelsFromApiKeyWorkflow,
} from "@medusajs/core-flows"
import { ApiKeyType, MedusaError } from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import { AdminApiKeySalesChannelType } from "../../../validators"
import { BatchMethodRequest } from "@medusajs/types"
import { refetchApiKey } from "../../../helpers"
export const POST = async (
req: AuthenticatedMedusaRequest<
BatchMethodRequest<AdminApiKeySalesChannelType, AdminApiKeySalesChannelType>
>,
res: MedusaResponse
) => {
const { create, delete: toDelete } = req.validatedBody
const apiKey = await refetchApiKey(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
if (apiKey.type !== ApiKeyType.PUBLISHABLE) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Sales channels can only be associated with publishable API keys"
)
}
if (create && create.length) {
const workflowInput = {
data: [
{
api_key_id: req.params.id,
sales_channel_ids: create ?? [],
},
],
}
const { errors } = await addSalesChannelsToApiKeyWorkflow(req.scope).run({
input: workflowInput,
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
}
if (toDelete && toDelete.length) {
const workflowInput = {
data: [
{
api_key_id: req.params.id,
sales_channel_ids: toDelete,
},
],
}
const { errors } = await removeSalesChannelsFromApiKeyWorkflow(
req.scope
).run({
input: workflowInput,
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
}
const newApiKey = await refetchApiKey(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ api_key: newApiKey })
}

View File

@@ -0,0 +1,23 @@
import { MedusaContainer } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export const refetchApiKey = async (
apiKeyId: string,
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "api_key",
variables: {
filters: { id: apiKeyId },
},
fields: fields,
})
const apiKeys = await remoteQuery(queryObject)
return apiKeys[0]
}

View File

@@ -1,18 +1,18 @@
import * as QueryConfig from "./query-config"
import { transformBody, transformQuery } from "../../../api/middlewares"
import {
AdminGetApiKeysApiKeyParams,
AdminGetApiKeysParams,
AdminPostApiKeysApiKeyReq,
AdminPostApiKeysApiKeySalesChannelsBatchAddReq,
AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq,
AdminPostApiKeysReq,
AdminRevokeApiKeysApiKeyReq,
} from "./validators"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
AdminApiKeySalesChannel,
AdminCreateApiKey,
AdminGetApiKeyParams,
AdminGetApiKeysParams,
AdminRevokeApiKey,
AdminUpdateApiKey,
} from "./validators"
import { validateAndTransformBody } from "../../utils/validate-body"
import { createBatchBody } from "../../utils/validators"
export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -23,7 +23,7 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"],
matcher: "/admin/api-keys",
middlewares: [
transformQuery(
validateAndTransformQuery(
AdminGetApiKeysParams,
QueryConfig.listTransformQueryConfig
),
@@ -33,8 +33,8 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"],
matcher: "/admin/api-keys/:id",
middlewares: [
transformQuery(
AdminGetApiKeysApiKeyParams,
validateAndTransformQuery(
AdminGetApiKeyParams,
QueryConfig.retrieveTransformQueryConfig
),
],
@@ -43,22 +43,22 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"],
matcher: "/admin/api-keys",
middlewares: [
transformQuery(
AdminGetApiKeysApiKeyParams,
validateAndTransformBody(AdminCreateApiKey),
validateAndTransformQuery(
AdminGetApiKeyParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminPostApiKeysReq),
],
},
{
method: ["POST"],
matcher: "/admin/api-keys/:id",
middlewares: [
transformQuery(
AdminGetApiKeysApiKeyParams,
validateAndTransformBody(AdminUpdateApiKey),
validateAndTransformQuery(
AdminGetApiKeyParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminPostApiKeysApiKeyReq),
],
},
{
@@ -70,33 +70,24 @@ export const adminApiKeyRoutesMiddlewares: MiddlewareRoute[] = [
method: ["POST"],
matcher: "/admin/api-keys/:id/revoke",
middlewares: [
transformQuery(
AdminGetApiKeysApiKeyParams,
validateAndTransformBody(AdminRevokeApiKey),
validateAndTransformQuery(
AdminGetApiKeyParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminRevokeApiKeysApiKeyReq),
],
},
{
method: ["POST"],
matcher: "/admin/api-keys/:id/sales-channels/batch/add",
matcher: "/admin/api-keys/:id/sales-channels/batch",
middlewares: [
transformQuery(
AdminGetApiKeysApiKeyParams,
validateAndTransformBody(
createBatchBody(AdminApiKeySalesChannel, AdminApiKeySalesChannel)
),
validateAndTransformQuery(
AdminGetApiKeyParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminPostApiKeysApiKeySalesChannelsBatchAddReq),
],
},
{
method: ["POST"],
matcher: "/admin/api-keys/:id/sales-channels/batch/remove",
middlewares: [
transformQuery(
AdminGetApiKeysApiKeyParams,
QueryConfig.retrieveTransformQueryConfig
),
transformBody(AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq),
],
},
]

View File

@@ -1,5 +1,4 @@
import { createApiKeysWorkflow } from "@medusajs/core-flows"
import { CreateApiKeyDTO } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
@@ -8,6 +7,7 @@ import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { AdminCreateApiKeyType } from "./validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
@@ -35,14 +35,14 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<Omit<CreateApiKeyDTO, "created_by">>,
req: AuthenticatedMedusaRequest<AdminCreateApiKeyType>,
res: MedusaResponse
) => {
const input = [
{
...req.validatedBody,
created_by: req.auth.actor_id,
} as CreateApiKeyDTO,
},
]
const { result, errors } = await createApiKeysWorkflow(req.scope).run({
@@ -54,7 +54,7 @@ export const POST = async (
throw errors[0].error
}
// We cannot use remoteQuery here, as we need to show the secret key in the response (and never again)
// We should not refetch the api key here, as we need to show the secret key in the response (and never again)
// And the only time we get to see the secret, is when we create it
res.status(200).json({ api_key: result[0] })
}

View File

@@ -1,88 +1,48 @@
import { ApiKeyType } from "@medusajs/utils"
import { Type } from "class-transformer"
import { z } from "zod"
import {
IsArray,
IsEnum,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
import { ApiKeyType } from "@medusajs/utils"
export class AdminGetApiKeysApiKeyParams extends FindParams {}
/**
* Parameters used to filter and configure the pagination of the retrieved api keys.
*/
export class AdminGetApiKeysParams extends extendedFindParamsMixin({
limit: 50,
export const AdminGetApiKeyParams = createSelectParams()
export type AdminGetApiKeysParamsType = z.infer<typeof AdminGetApiKeysParams>
export const AdminGetApiKeysParams = createFindParams({
offset: 0,
}) {
/**
* Search parameter for api keys.
*/
@IsString({ each: true })
@IsOptional()
id?: string | string[]
limit: 50,
}).merge(
z.object({
id: z.union([z.string(), z.array(z.string())]).optional(),
title: z.union([z.string(), z.array(z.string())]).optional(),
token: z.union([z.string(), z.array(z.string())]).optional(),
type: z.nativeEnum(ApiKeyType).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
$and: z.lazy(() => AdminGetApiKeysParams.array()).optional(),
$or: z.lazy(() => AdminGetApiKeysParams.array()).optional(),
})
)
/**
* Filter by title
*/
@IsString({ each: true })
@IsOptional()
title?: string | string[]
export type AdminCreateApiKeyType = z.infer<typeof AdminCreateApiKey>
export const AdminCreateApiKey = z.object({
title: z.string(),
type: z.nativeEnum(ApiKeyType),
})
/**
* Filter by token
*/
@IsString({ each: true })
@IsOptional()
token?: string | string[]
export type AdminUpdateApiKeyType = z.infer<typeof AdminUpdateApiKey>
export const AdminUpdateApiKey = z.object({
title: z.string(),
})
/**
* Filter by type
*/
@IsEnum(ApiKeyType, { each: true })
@IsOptional()
type?: ApiKeyType
export type AdminRevokeApiKeyType = z.infer<typeof AdminRevokeApiKey>
export const AdminRevokeApiKey = z.object({
revoke_in: z.number().optional(),
})
// Additional filters from BaseFilterable
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetApiKeysParams)
$and?: AdminGetApiKeysParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetApiKeysParams)
$or?: AdminGetApiKeysParams[]
}
export class AdminPostApiKeysReq {
@IsString()
title: string
@IsEnum(ApiKeyType, {})
type: ApiKeyType
}
export class AdminPostApiKeysApiKeyReq {
@IsString()
title: string
}
export class AdminRevokeApiKeysApiKeyReq {
@IsOptional()
@IsNumber()
revoke_in?: number
}
export class AdminDeleteApiKeysApiKeyReq {}
export class AdminPostApiKeysApiKeySalesChannelsBatchAddReq {
@IsArray()
sales_channel_ids: string[]
}
// eslint-disable-next-line max-len
export class AdminPostApiKeysApiKeySalesChannelsBatchRemoveReq extends AdminPostApiKeysApiKeySalesChannelsBatchAddReq {}
export type AdminApiKeySalesChannelType = z.infer<
typeof AdminApiKeySalesChannel
>
export const AdminApiKeySalesChannel = z.string()

View File

@@ -1,6 +1,5 @@
import {
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import {
@@ -8,32 +7,32 @@ import {
updateCampaignsWorkflow,
} from "@medusajs/core-flows"
import { AdminPostCampaignsReq } from "../validators"
import { IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { UpdateCampaignDTO } from "@medusajs/types"
import { refetchCampaign } from "../helpers"
import { AdminUpdateCampaignType } from "../validators"
import { MedusaError } from "@medusajs/utils"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const promotionModuleService: IPromotionModuleService = req.scope.resolve(
ModuleRegistrationName.PROMOTION
const campaign = await refetchCampaign(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
const campaign = await promotionModuleService.retrieveCampaign(
req.params.id,
{
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
}
)
if (!campaign) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Campaign with id: ${req.params.id} was not found`
)
}
res.status(200).json({ campaign })
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostCampaignsReq>,
req: AuthenticatedMedusaRequest<AdminUpdateCampaignType>,
res: MedusaResponse
) => {
const updateCampaigns = updateCampaignsWorkflow(req.scope)
@@ -42,7 +41,7 @@ export const POST = async (
id: req.params.id,
...req.validatedBody,
},
] as UpdateCampaignDTO[]
]
const { result, errors } = await updateCampaigns.run({
input: { campaignsData },
@@ -53,7 +52,12 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ campaign: result[0] })
const campaign = await refetchCampaign(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ campaign })
}
export const DELETE = async (

View File

@@ -0,0 +1,23 @@
import { MedusaContainer } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export const refetchCampaign = async (
campaignId: string,
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "campaign",
variables: {
filters: { id: campaignId },
},
fields: fields,
})
const campaigns = await remoteQuery(queryObject)
return campaigns[0]
}

View File

@@ -1,2 +0,0 @@
export * from "./types"
export * from "./validators"

View File

@@ -1,31 +1,25 @@
import * as QueryConfig from "./query-config"
import {
AdminGetCampaignsCampaignParams,
AdminGetCampaignsParams,
AdminPostCampaignsCampaignReq,
AdminPostCampaignsReq,
} from "./validators"
import {
isFeatureFlagEnabled,
transformBody,
transformQuery,
} from "../../../api/middlewares"
import { MedusaV2Flag } from "@medusajs/utils"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
AdminCreateCampaign,
AdminGetCampaignParams,
AdminGetCampaignsParams,
AdminUpdateCampaign,
} from "./validators"
import { validateAndTransformBody } from "../../utils/validate-body"
export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [
{
matcher: "/admin/campaigns*",
middlewares: [authenticate("admin", ["bearer", "session"])],
middlewares: [authenticate("admin", ["bearer", "session", "api-key"])],
},
{
method: ["GET"],
matcher: "/admin/campaigns",
middlewares: [
transformQuery(
validateAndTransformQuery(
AdminGetCampaignsParams,
QueryConfig.listTransformQueryConfig
),
@@ -34,14 +28,20 @@ export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
matcher: "/admin/campaigns",
middlewares: [transformBody(AdminPostCampaignsReq)],
middlewares: [
validateAndTransformBody(AdminCreateCampaign),
validateAndTransformQuery(
AdminGetCampaignParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/campaigns/:id",
middlewares: [
transformQuery(
AdminGetCampaignsCampaignParams,
validateAndTransformQuery(
AdminGetCampaignParams,
QueryConfig.retrieveTransformQueryConfig
),
],
@@ -49,6 +49,12 @@ export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
matcher: "/admin/campaigns/:id",
middlewares: [transformBody(AdminPostCampaignsCampaignReq)],
middlewares: [
validateAndTransformBody(AdminUpdateCampaign),
validateAndTransformQuery(
AdminGetCampaignParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
]

View File

@@ -1,13 +1,10 @@
export const defaultAdminCampaignRelations = ["budget"]
export const allowedAdminCampaignRelations = [
...defaultAdminCampaignRelations,
"promotions",
]
export const defaultAdminCampaignFields = [
"id",
"name",
"description",
"currency",
"campaign_identifier",
"*budget",
"starts_at",
"ends_at",
"created_at",
@@ -16,9 +13,7 @@ export const defaultAdminCampaignFields = [
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminCampaignFields,
defaultRelations: defaultAdminCampaignRelations,
allowedRelations: allowedAdminCampaignRelations,
defaults: defaultAdminCampaignFields,
isList: false,
}

View File

@@ -1,38 +1,42 @@
import {
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { CreateCampaignDTO, IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createCampaignsWorkflow } from "@medusajs/core-flows"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { AdminCreateCampaignType } from "./validators"
import { refetchCampaign } from "./helpers"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const promotionModuleService: IPromotionModuleService = req.scope.resolve(
ModuleRegistrationName.PROMOTION
)
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const [campaigns, count] = await promotionModuleService.listAndCountCampaigns(
req.filterableFields,
req.listConfig
)
const query = remoteQueryObjectFromString({
entryPoint: "campaign",
variables: {
filters: req.filterableFields,
...req.remoteQueryConfig.pagination,
},
fields: req.remoteQueryConfig.fields,
})
const { limit, offset } = req.validatedQuery
const { rows: campaigns, metadata } = await remoteQuery(query)
res.json({
count,
campaigns,
offset,
limit,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<CreateCampaignDTO>,
req: AuthenticatedMedusaRequest<AdminCreateCampaignType>,
res: MedusaResponse
) => {
const createCampaigns = createCampaignsWorkflow(req.scope)
@@ -50,5 +54,11 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ campaign: result[0] })
const campaign = await refetchCampaign(
result[0].id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ campaign })
}

View File

@@ -1,9 +0,0 @@
import { CampaignDTO, PaginatedResponse } from "@medusajs/types"
export type AdminCampaignsListRes = PaginatedResponse<{
campaigns: CampaignDTO[]
}>
export type AdminCampaignRes = {
campaign: CampaignDTO
}

View File

@@ -1,119 +1,54 @@
import { Type } from "class-transformer"
import {
IsArray,
IsDateString,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { CampaignBudgetType } from "@medusajs/utils"
import { createFindParams, createSelectParams } from "../../utils/validators"
import { z } from "zod"
export class AdminGetCampaignsCampaignParams extends FindParams {}
export const AdminGetCampaignParams = createSelectParams()
export class AdminGetCampaignsParams extends extendedFindParamsMixin({
limit: 100,
export type AdminGetCampaignsParamsType = z.infer<
typeof AdminGetCampaignsParams
>
export const AdminGetCampaignsParams = createFindParams({
offset: 0,
}) {
@IsString()
@IsOptional()
campaign_identifier?: string
limit: 50,
}).merge(
z.object({
campaign_identifier: z.string().optional(),
currency: z.string().optional(),
$and: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
$or: z.lazy(() => AdminGetCampaignsParams.array()).optional(),
})
)
@IsString()
@IsOptional()
currency?: string
}
const CreateCampaignBudget = z.object({
type: z.nativeEnum(CampaignBudgetType),
limit: z.number(),
})
export class AdminPostCampaignsReq {
@IsNotEmpty()
@IsString()
name: string
const UpdateCampaignBudget = z.object({
type: z.nativeEnum(CampaignBudgetType).optional(),
limit: z.number().optional(),
})
@IsOptional()
@IsNotEmpty()
campaign_identifier?: string
export type AdminCreateCampaignType = z.infer<typeof AdminCreateCampaign>
export const AdminCreateCampaign = z.object({
name: z.string(),
campaign_identifier: z.string(),
description: z.string().optional(),
currency: z.string().optional(),
budget: CreateCampaignBudget.optional(),
starts_at: z.coerce.date(),
ends_at: z.coerce.date(),
promotions: z.array(z.object({ id: z.string() })).optional(),
})
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
currency?: string
@IsOptional()
@ValidateNested()
@Type(() => CampaignBudget)
budget?: CampaignBudget
@IsOptional()
@IsDateString()
starts_at?: string
@IsOptional()
@IsDateString()
ends_at?: string
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => IdObject)
promotions?: IdObject[]
}
export class IdObject {
@IsString()
@IsNotEmpty()
id: string
}
export class CampaignBudget {
@IsOptional()
@IsEnum(CampaignBudgetType)
type?: CampaignBudgetType
@IsOptional()
@IsNumber()
limit?: number
}
export class AdminPostCampaignsCampaignReq {
@IsOptional()
@IsString()
name?: string
@IsOptional()
@IsNotEmpty()
campaign_identifier?: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
currency?: string
@IsOptional()
@ValidateNested()
@Type(() => CampaignBudget)
budget?: CampaignBudget
@IsOptional()
@IsDateString()
starts_at?: string
@IsOptional()
@IsDateString()
ends_at?: string
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => IdObject)
promotions?: IdObject[]
}
export type AdminUpdateCampaignType = z.infer<typeof AdminUpdateCampaign>
export const AdminUpdateCampaign = z.object({
name: z.string().optional(),
campaign_identifier: z.string().optional(),
description: z.string().optional(),
currency: z.string().optional(),
budget: UpdateCampaignBudget.optional(),
starts_at: z.coerce.date().optional(),
ends_at: z.coerce.date().optional(),
promotions: z.array(z.object({ id: z.string() })).optional(),
})

View File

@@ -7,30 +7,24 @@ import {
updateCollectionsWorkflow,
} from "@medusajs/core-flows"
import { UpdateProductCollectionDTO } from "@medusajs/types"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import { AdminUpdateCollectionType } from "../validators"
import { refetchCollection } from "../helpers"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const variables = { id: req.params.id }
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_collection",
variables,
fields: req.retrieveConfig.select as string[],
})
const [collection] = await remoteQuery(queryObject)
const collection = await refetchCollection(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ collection })
}
export const POST = async (
req: AuthenticatedMedusaRequest<UpdateProductCollectionDTO>,
req: AuthenticatedMedusaRequest<AdminUpdateCollectionType>,
res: MedusaResponse
) => {
const { result, errors } = await updateCollectionsWorkflow(req.scope).run({
@@ -45,7 +39,13 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ collection: result[0] })
const collection = await refetchCollection(
req.params.id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ collection })
}
export const DELETE = async (

View File

@@ -0,0 +1,23 @@
import { MedusaContainer } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
export const refetchCollection = async (
collectionId: string,
scope: MedusaContainer,
fields: string[]
) => {
const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_collection",
variables: {
filters: { id: collectionId },
},
fields: fields,
})
const collections = await remoteQuery(queryObject)
return collections[0]
}

View File

@@ -1,15 +1,14 @@
import * as QueryConfig from "./query-config"
import {
AdminGetCollectionsCollectionParams,
AdminGetCollectionsParams,
AdminPostCollectionsCollectionReq,
AdminPostCollectionsReq,
} from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
import { validateAndTransformQuery } from "../../utils/validate-query"
import {
AdminCreateCollection,
AdminGetCollectionParams,
AdminGetCollectionsParams,
AdminUpdateCollection,
} from "./validators"
import { validateAndTransformBody } from "../../utils/validate-body"
export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
{
@@ -22,7 +21,7 @@ export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"],
matcher: "/admin/collections",
middlewares: [
transformQuery(
validateAndTransformQuery(
AdminGetCollectionsParams,
QueryConfig.listTransformQueryConfig
),
@@ -32,8 +31,8 @@ export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
method: ["GET"],
matcher: "/admin/collections/:id",
middlewares: [
transformQuery(
AdminGetCollectionsCollectionParams,
validateAndTransformQuery(
AdminGetCollectionParams,
QueryConfig.retrieveTransformQueryConfig
),
],
@@ -41,12 +40,24 @@ export const adminCollectionRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
matcher: "/admin/collections",
middlewares: [transformBody(AdminPostCollectionsReq)],
middlewares: [
validateAndTransformBody(AdminCreateCollection),
validateAndTransformQuery(
AdminGetCollectionParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/collections/:id",
middlewares: [transformBody(AdminPostCollectionsCollectionReq)],
middlewares: [
validateAndTransformBody(AdminUpdateCollection),
validateAndTransformQuery(
AdminGetCollectionParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["DELETE"],

View File

@@ -1,8 +1,3 @@
export const allowedAdminCollectionRelations = ["products.profiles"]
// TODO: See how these should look when expanded
export const defaultAdminCollectionRelations = ["products.profiles"]
export const defaultAdminCollectionFields = [
"id",
"title",
@@ -12,9 +7,7 @@ export const defaultAdminCollectionFields = [
]
export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminCollectionFields,
defaultRelations: defaultAdminCollectionRelations,
allowedRelations: allowedAdminCollectionRelations,
defaults: defaultAdminCollectionFields,
isList: false,
}

View File

@@ -3,28 +3,30 @@ import {
MedusaResponse,
} from "../../../types/routing"
import { CreateProductCollectionDTO } from "@medusajs/types"
import { createCollectionsWorkflow } from "@medusajs/core-flows"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { AdminCreateCollectionType } from "./validators"
import { refetchCollection } from "./helpers"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
const query = remoteQueryObjectFromString({
entryPoint: "product_collection",
variables: {
filters: req.filterableFields,
order: req.listConfig.order,
skip: req.listConfig.skip,
take: req.listConfig.take,
...req.remoteQueryConfig.pagination,
},
fields: req.listConfig.select as string[],
fields: req.remoteQueryConfig.fields,
})
const { rows: collections, metadata } = await remoteQuery(queryObject)
const { rows: collections, metadata } = await remoteQuery(query)
res.json({
collections,
@@ -35,7 +37,7 @@ export const GET = async (
}
export const POST = async (
req: AuthenticatedMedusaRequest<CreateProductCollectionDTO>,
req: AuthenticatedMedusaRequest<AdminCreateCollectionType>,
res: MedusaResponse
) => {
const input = [
@@ -53,5 +55,11 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ collection: result[0] })
const collection = await refetchCollection(
result[0].id,
req.scope,
req.remoteQueryConfig.fields
)
res.status(200).json({ collection })
}

View File

@@ -1,115 +1,41 @@
import { OperatorMap } from "@medusajs/types"
import { Type } from "class-transformer"
import {
IsNotEmpty,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
import { z } from "zod"
// TODO: Ensure these match the DTOs in the types
export class AdminGetCollectionsCollectionParams extends FindParams {}
export const AdminGetCollectionParams = createSelectParams()
/**
* Parameters used to filter and configure the pagination of the retrieved regions.
*/
export class AdminGetCollectionsParams extends extendedFindParamsMixin({
limit: 10,
export type AdminGetCollectionsParamsType = z.infer<
typeof AdminGetCollectionsParams
>
export const AdminGetCollectionsParams = createFindParams({
offset: 0,
}) {
/**
* Term to search product collections by their title and handle.
*/
@IsString()
@IsOptional()
q?: string
limit: 10,
}).merge(
z.object({
q: z.string().optional(),
title: z.union([z.string(), z.array(z.string())]).optional(),
handle: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
$and: z.lazy(() => AdminGetCollectionsParams.array()).optional(),
$or: z.lazy(() => AdminGetCollectionsParams.array()).optional(),
})
)
/**
* Title to filter product collections by.
*/
@IsOptional()
@IsString()
title?: string | string[]
export type AdminCreateCollectionType = z.infer<typeof AdminCreateCollection>
export const AdminCreateCollection = z.object({
title: z.string(),
handle: z.string().optional(),
metadata: z.record(z.unknown()).optional(),
})
/**
* Handle to filter product collections by.
*/
@IsOptional()
@IsString()
handle?: string | string[]
/**
* Date filters to apply on the product collections' `created_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
created_at?: OperatorMap<string>
/**
* Date filters to apply on the product collections' `updated_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => OperatorMapValidator)
updated_at?: OperatorMap<string>
/**
* Date filters to apply on the product collections' `deleted_at` date.
*/
@ValidateNested()
@IsOptional()
@Type(() => OperatorMapValidator)
deleted_at?: OperatorMap<string>
// TODO: To be added in next iteration
// /**
// * Filter product collections by their associated discount condition's ID.
// */
// @IsString()
// @IsOptional()
// discount_condition_id?: string
// Note: These are new in v2
// Additional filters from BaseFilterable
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetCollectionsParams)
$and?: AdminGetCollectionsParams[]
@IsOptional()
@ValidateNested({ each: true })
@Type(() => AdminGetCollectionsParams)
$or?: AdminGetCollectionsParams[]
}
export class AdminPostCollectionsReq {
@IsString()
@IsNotEmpty()
title: string
@IsString()
@IsOptional()
handle?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
export class AdminPostCollectionsCollectionReq {
@IsString()
@IsOptional()
title?: string
@IsString()
@IsOptional()
handle?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
export type AdminUpdateCollectionType = z.infer<typeof AdminUpdateCollection>
export const AdminUpdateCollection = z.object({
title: z.string().optional(),
handle: z.string().optional(),
metadata: z.record(z.unknown()).optional(),
})

View File

@@ -1,8 +1,11 @@
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const variables = { code: req.params.code }

View File

@@ -1,8 +1,11 @@
import { remoteQueryObjectFromString } from "@medusajs/utils"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const remoteQuery = req.scope.resolve("remoteQuery")
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "currency",

View File

@@ -1,2 +1 @@
export * from "./campaigns"
export * from "./promotions"

View File

@@ -3,6 +3,7 @@ import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
ApplicationMethodType,
CampaignBudgetType,
PromotionRuleOperator,
PromotionType,
} from "@medusajs/utils"
@@ -11,6 +12,7 @@ import {
ArrayNotEmpty,
IsArray,
IsBoolean,
IsDateString,
IsEnum,
IsNotEmpty,
IsNumber,
@@ -26,7 +28,6 @@ import {
extendedFindParamsMixin,
} from "../../../types/common"
import { XorConstraint } from "../../../types/validators/xor"
import { AdminPostCampaignsReq } from "../campaigns/validators"
export class AdminGetPromotionsPromotionParams extends FindParams {}
export class AdminGetPromotionRules extends FindParams {}
@@ -92,6 +93,59 @@ export class AdminPostCreatePromotionRule {
values: string[]
}
export class AdminPostCampaignsReq {
@IsNotEmpty()
@IsString()
name: string
@IsOptional()
@IsNotEmpty()
campaign_identifier?: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
currency?: string
@IsOptional()
@ValidateNested()
@Type(() => CampaignBudget)
budget?: CampaignBudget
@IsOptional()
@IsDateString()
starts_at?: string
@IsOptional()
@IsDateString()
ends_at?: string
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => IdObject)
promotions?: IdObject[]
}
export class IdObject {
@IsString()
@IsNotEmpty()
id: string
}
export class CampaignBudget {
@IsOptional()
@IsEnum(CampaignBudgetType)
type?: CampaignBudgetType
@IsOptional()
@IsNumber()
limit?: number
}
export class AdminPostPromotionsReq {
@IsNotEmpty()
@IsString()