feat: implement direct upload (#12328)
* feat: implement direct upload * feat: add direct-upload endpoint * refactor: implement feedback * refactor: have a dedicated endpoint for direct uploads * refactor: convert responses to snakecase * refactor: rename method to createImport * test: add tests for the presigned-urls endpoint
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "@medusajs/framework/http"
|
||||
|
||||
import {
|
||||
importProductsWorkflowId,
|
||||
waitConfirmationProductImportStepId,
|
||||
} from "@medusajs/core-flows"
|
||||
import { IWorkflowEngineService } from "@medusajs/framework/types"
|
||||
import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
|
||||
import { StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const workflowEngineService: IWorkflowEngineService = req.scope.resolve(
|
||||
Modules.WORKFLOW_ENGINE
|
||||
)
|
||||
const transactionId = req.params.transaction_id
|
||||
|
||||
await workflowEngineService.setStepSuccess({
|
||||
idempotencyKey: {
|
||||
action: TransactionHandlerType.INVOKE,
|
||||
transactionId,
|
||||
stepId: waitConfirmationProductImportStepId,
|
||||
workflowId: importProductsWorkflowId,
|
||||
},
|
||||
stepResponse: new StepResponse(true),
|
||||
})
|
||||
|
||||
res.status(202).json({})
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
MedusaResponse,
|
||||
AuthenticatedMedusaRequest,
|
||||
} from "@medusajs/framework/http"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import type { HttpTypes } from "@medusajs/framework/types"
|
||||
import { importProductsWorkflow } from "@medusajs/core-flows"
|
||||
import type { AdminImportProductsType } from "../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminImportProductsType>,
|
||||
res: MedusaResponse<HttpTypes.AdminImportProductResponse>
|
||||
) => {
|
||||
const fileProvider = req.scope.resolve(Modules.FILE)
|
||||
const file = await fileProvider.getAsBuffer(req.validatedBody.file_key)
|
||||
|
||||
const { result, transaction } = await importProductsWorkflow(req.scope).run({
|
||||
input: {
|
||||
filename: req.validatedBody.originalname,
|
||||
fileContent: file.toString("utf-8"),
|
||||
},
|
||||
})
|
||||
|
||||
res
|
||||
.status(202)
|
||||
.json({ transaction_id: transaction.transactionId, summary: result })
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
validateAndTransformBody,
|
||||
validateAndTransformQuery,
|
||||
} from "@medusajs/framework"
|
||||
import { maybeApplyLinkFilter, MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import multer from "multer"
|
||||
import { maybeApplyLinkFilter, MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares"
|
||||
import { createBatchBody } from "../../utils/validators"
|
||||
import * as QueryConfig from "./query-config"
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
AdminGetProductsParams,
|
||||
AdminGetProductVariantParams,
|
||||
AdminGetProductVariantsParams,
|
||||
AdminImportProducts,
|
||||
AdminUpdateProduct,
|
||||
AdminUpdateProductOption,
|
||||
AdminUpdateProductVariant,
|
||||
@@ -34,9 +35,6 @@ import {
|
||||
} from "./validators"
|
||||
import IndexEngineFeatureFlag from "../../../loaders/feature-flags/index-engine"
|
||||
|
||||
// TODO: For now we keep the files in memory, as that's how they get passed to the workflows
|
||||
// This will need revisiting once we are closer to prod-ready v2, since with workflows and potentially
|
||||
// services on other machines using streams is not as simple as it used to be.
|
||||
const upload = multer({ storage: multer.memoryStorage() })
|
||||
|
||||
export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
@@ -104,6 +102,11 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/admin/products/import",
|
||||
middlewares: [upload.single("file")],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/products/imports",
|
||||
middlewares: [validateAndTransformBody(AdminImportProducts)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/products/import/:transaction_id/confirm",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BatchMethodRequest } from "@medusajs/framework/types"
|
||||
import { BatchMethodRequest, HttpTypes } from "@medusajs/framework/types"
|
||||
import { ProductStatus } from "@medusajs/framework/utils"
|
||||
import { z } from "zod"
|
||||
import { z, ZodType } from "zod"
|
||||
import {
|
||||
applyAndAndOrOperators,
|
||||
booleanString,
|
||||
@@ -341,3 +341,12 @@ export type AdminBatchVariantInventoryItemsType = BatchMethodRequest<
|
||||
AdminBatchUpdateVariantInventoryItemType,
|
||||
AdminBatchDeleteVariantInventoryItemType
|
||||
>
|
||||
|
||||
export const AdminImportProducts = z.object({
|
||||
file_key: z.string(),
|
||||
originalname: z.string(),
|
||||
extension: z.string(),
|
||||
size: z.number(),
|
||||
mime_type: z.string(),
|
||||
}) satisfies ZodType<HttpTypes.AdminImportProductsRequest>
|
||||
export type AdminImportProductsType = z.infer<typeof AdminImportProducts>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import multer from "multer"
|
||||
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||
import {
|
||||
MiddlewareRoute,
|
||||
validateAndTransformBody,
|
||||
} from "@medusajs/framework/http"
|
||||
import { validateAndTransformQuery } from "@medusajs/framework"
|
||||
import { retrieveUploadConfig } from "./query-config"
|
||||
import { AdminGetUploadParams } from "./validators"
|
||||
import { AdminGetUploadParams, AdminUploadPreSignedUrl } from "./validators"
|
||||
|
||||
// TODO: For now we keep the files in memory, as that's how they get passed to the workflows
|
||||
// This will need revisiting once we are closer to prod-ready v2, since with workflows and potentially
|
||||
@@ -31,4 +34,9 @@ export const adminUploadRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/admin/uploads/:id",
|
||||
middlewares: [],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/uploads/presigned-urls",
|
||||
middlewares: [validateAndTransformBody(AdminUploadPreSignedUrl)],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ulid } from "ulid"
|
||||
import { MIMEType } from "util"
|
||||
import type {
|
||||
MedusaResponse,
|
||||
AuthenticatedMedusaRequest,
|
||||
} from "@medusajs/framework/http"
|
||||
import {
|
||||
Modules,
|
||||
MedusaError,
|
||||
MedusaErrorTypes,
|
||||
} from "@medusajs/framework/utils"
|
||||
import type { HttpTypes } from "@medusajs/framework/types"
|
||||
import type { AdminUploadPreSignedUrlType } from "../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest<AdminUploadPreSignedUrlType>,
|
||||
res: MedusaResponse<HttpTypes.AdminUploadPreSignedUrlResponse>
|
||||
) => {
|
||||
const fileProvider = req.scope.resolve(Modules.FILE)
|
||||
let type: MIMEType
|
||||
|
||||
try {
|
||||
type = new MIMEType(req.validatedBody.mime_type)
|
||||
} catch {
|
||||
throw new MedusaError(
|
||||
MedusaErrorTypes.INVALID_DATA,
|
||||
`Invalid file type "${req.validatedBody.mime_type}"`,
|
||||
MedusaErrorTypes.INVALID_DATA
|
||||
)
|
||||
}
|
||||
|
||||
const extension = type.subtype
|
||||
const uniqueFilename = `${ulid()}.${extension}`
|
||||
|
||||
const response = await fileProvider.getUploadFileUrls({
|
||||
filename: uniqueFilename,
|
||||
mimeType: req.validatedBody.mime_type,
|
||||
access: req.validatedBody.access ?? "private",
|
||||
})
|
||||
|
||||
res.json({
|
||||
url: response.url,
|
||||
filename: response.key,
|
||||
mime_type: type.toString(),
|
||||
size: req.validatedBody.size,
|
||||
extension,
|
||||
originalname: req.validatedBody.originalname,
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
import { z, ZodType } from "zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { createSelectParams } from "../../utils/validators"
|
||||
import { z } from "zod"
|
||||
|
||||
export type AdminGetUploadParamsType = z.infer<typeof AdminGetUploadParams>
|
||||
export const AdminGetUploadParams = createSelectParams()
|
||||
|
||||
export const AdminUploadPreSignedUrl = z.object({
|
||||
originalname: z.string(),
|
||||
mime_type: z.string(),
|
||||
size: z.number(),
|
||||
access: z.enum(["public", "private"]).optional(),
|
||||
}) satisfies ZodType<HttpTypes.AdminUploadPreSignedUrlRequest>
|
||||
|
||||
export type AdminUploadPreSignedUrlType = z.infer<
|
||||
typeof AdminUploadPreSignedUrl
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user