Feat(medusa-js, medusa-react): Upload endpoints in medusa js and react (#1716)

* export types from admin uploads

* add delete and download to medusa-js

* add upload endpoints to hooks

* remove upload from js and react

* pr feedback

* Apply suggestions from code review

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>

* rename types for admin uploads

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Philip KorsholmandOliver Windall Juhl authored and GitHub committed 2022-06-29 11:36:19 +02:00
1 parent bf47d1aecd
commit 9018d270be
11 files changed
+187 -25

No files matched your search

@@ -1,14 +1,21 @@
import { AdminUploadRes, IAdminPostUploadsFile } from "@medusajs/medusa" import {
AdminDeleteUploadsReq,
IAdminPostUploadsFileReq,
AdminDeleteUploadsRes,
AdminPostUploadsDownloadUrlReq,
AdminUploadsDownloadUrlRes,
AdminUploadsRes,
} from "@medusajs/medusa"
import FormData from "form-data"
import { ResponsePromise } from "../../typings" import { ResponsePromise } from "../../typings"
import BaseResource from "../base" import BaseResource from "../base"
import FormData from "form-data"
class AdminUploadsResource extends BaseResource { class AdminUploadsResource extends BaseResource {
private headers = { private headers = {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
} }
create(file: IAdminPostUploadsFile): ResponsePromise<AdminUploadRes> { create(file: IAdminPostUploadsFileReq): ResponsePromise<AdminUploadsRes> {
const path = `/admin/uploads` const path = `/admin/uploads`
const payload = new FormData() const payload = new FormData()
@@ -16,6 +23,24 @@ class AdminUploadsResource extends BaseResource {
return this.client.request("POST", path, payload, {}, this.headers) return this.client.request("POST", path, payload, {}, this.headers)
} }
delete(
payload: AdminDeleteUploadsReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminDeleteUploadsRes> {
const path = `/admin/uploads`
return this.client.request("DELETE", path, payload, {}, customHeaders)
}
getPresignedDownloadUrl(
payload: AdminPostUploadsDownloadUrlReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminUploadsDownloadUrlRes> {
const path = `/admin/uploads/download-url`
return this.client.request("POST", path, payload, {}, customHeaders)
}
} }
export default AdminUploadsResource export default AdminUploadsResource
@@ -1258,6 +1258,9 @@
"updated_at": "2021-03-16T21:24:00.389Z", "updated_at": "2021-03-16T21:24:00.389Z",
"created_at": "2021-03-16T21:24:00.389Z", "created_at": "2021-03-16T21:24:00.389Z",
"deleted_at": null "deleted_at": null
},
"upload": {
"url": "test-url"
} }
} }
} }
@@ -1,4 +1,5 @@
import { rest } from "msw" import { rest } from "msw"
import { body } from "msw/lib/types/context"
import { fixtures } from "../data" import { fixtures } from "../data"
export const adminHandlers = [ export const adminHandlers = [
@@ -1651,4 +1652,24 @@ export const adminHandlers = [
rest.delete("/admin/auth", (req, res, ctx) => { rest.delete("/admin/auth", (req, res, ctx) => {
return res(ctx.status(200)) return res(ctx.status(200))
}), }),
rest.delete("/admin/uploads", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: (req.body as any).file_key,
object: "file",
deleted: true,
})
)
}),
rest.post("/admin/uploads/download-url", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
download_url: fixtures.get("upload").url,
})
)
}),
] ]
@@ -25,3 +25,4 @@ export * from "./swaps"
export * from "./tax-rates" export * from "./tax-rates"
export * from "./users" export * from "./users"
export * from "./variants" export * from "./variants"
export * from "./uploads"
@@ -0,0 +1 @@
export * from "./mutations"
@@ -0,0 +1,60 @@
import {
AdminDeleteUploadsReq,
IAdminPostUploadsFileReq,
AdminDeleteUploadsRes,
AdminPostUploadsDownloadUrlReq,
AdminUploadsDownloadUrlRes,
AdminUploadsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions"
export const useAdminUploadFile = (
options?: UseMutationOptions<
Response<AdminUploadsRes>,
Error,
IAdminPostUploadsFileReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation((payload: IAdminPostUploadsFileReq) => {
return client.admin.uploads.create(payload)
}, buildOptions(queryClient, [], options))
}
export const useAdminCreatePresignedDownloadUrl = (
options?: UseMutationOptions<
Response<AdminUploadsDownloadUrlRes>,
Error,
AdminPostUploadsDownloadUrlReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostUploadsDownloadUrlReq) =>
client.admin.uploads.getPresignedDownloadUrl(payload),
buildOptions(queryClient, [], options)
)
}
export const useAdminDeleteFile = (
options?: UseMutationOptions<
Response<AdminDeleteUploadsRes>,
Error,
AdminDeleteUploadsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminDeleteUploadsReq) => client.admin.uploads.delete(payload),
buildOptions(queryClient, [], options)
)
}
@@ -0,0 +1,46 @@
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import {
useAdminDeleteFile,
useAdminCreatePresignedDownloadUrl,
} from "../../../../src"
import { createWrapper } from "../../../utils"
describe("useAdminDeleteFile hook", () => {
test("Removes file with key and returns deleteresult", async () => {
const file_key = "test"
const { result, waitFor } = renderHook(() => useAdminDeleteFile(), {
wrapper: createWrapper(),
})
result.current.mutate({ file_key })
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({ id: file_key, object: "file", deleted: true })
)
})
})
describe("useAdminCreatePresignedDownloadUrl hook", () => {
test("", async () => {
const file_key = "test"
const { result, waitFor } = renderHook(
() => useAdminCreatePresignedDownloadUrl(),
{
wrapper: createWrapper(),
}
)
result.current.mutate({ file_key })
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.download_url).toEqual(fixtures.get("upload").url)
})
})
@@ -37,7 +37,7 @@ export default async (req, res) => {
} }
} }
export class IAdminPostUploadsFile { export class IAdminPostUploadsFileReq {
originalName: string originalName: string
path: string path: string
} }
@@ -1,9 +1,8 @@
import { IsString } from "class-validator" import { IsString } from "class-validator"
import { validator } from "../../../../utils/validator"
/** /**
* [delete] /uploads * [delete] /uploads
* operationId: "AdminDeleteUpload" * operationId: "AdminDeleteUploads"
* summary: "Removes an uploaded file" * summary: "Removes an uploaded file"
* description: "Removes an uploaded file using the installed fileservice" * description: "Removes an uploaded file using the installed fileservice"
* x-authenticated: true * x-authenticated: true
@@ -14,9 +13,7 @@ import { validator } from "../../../../utils/validator"
* description: OK * description: OK
*/ */
export default async (req, res) => { export default async (req, res) => {
const validated = await validator(AdminDeleteUploadReq, req.body, { const validated = req.validatedBody as AdminDeleteUploadsReq
forbidUnknownValues: false,
})
const fileService = req.scope.resolve("fileService") const fileService = req.scope.resolve("fileService")
@@ -27,7 +24,7 @@ export default async (req, res) => {
.send({ id: validated.file_key, object: "file", deleted: true }) .send({ id: validated.file_key, object: "file", deleted: true })
} }
class AdminDeleteUploadReq { export class AdminDeleteUploadsReq {
@IsString() @IsString()
file_key: string file_key: string
} }
@@ -1,12 +1,11 @@
import { IsString } from "class-validator" import { IsString } from "class-validator"
import { AbstractFileService } from "../../../../interfaces" import { AbstractFileService } from "../../../../interfaces"
import { validator } from "../../../../utils/validator"
/** /**
* [get] /uploads * [post] /uploads/download-url
* operationId: "GetUploadsFileDownloadUrl" * operationId: "PostUploadsDownloadUrl"
* summary: "Gets a presigned download url for a file" * summary: "Creates a presigned download url for a file"
* description: "Gets a presigned download url for a file" * description: "Creates a presigned download url for a file"
* x-authenticated: true * x-authenticated: true
* requestBody: * requestBody:
* content: * content:
@@ -26,18 +25,16 @@ import { validator } from "../../../../utils/validator"
* description: OK * description: OK
*/ */
export default async (req, res) => { export default async (req, res) => {
const validated = await validator(AdminGetUploadsFileDownloadUrlReq, req.body)
const fileService: AbstractFileService<any> = req.scope.resolve("fileService") const fileService: AbstractFileService<any> = req.scope.resolve("fileService")
const url = await fileService.getPresignedDownloadUrl({ const url = await fileService.getPresignedDownloadUrl({
fileKey: validated.file_key, fileKey: (req.validatedBody as AdminPostUploadsDownloadUrlReq).file_key,
}) })
res.status(200).send({ download_url: url }) res.status(200).send({ download_url: url })
} }
class AdminGetUploadsFileDownloadUrlReq { export class AdminPostUploadsDownloadUrlReq {
@IsString() @IsString()
file_key: string file_key: string
} }
@@ -2,7 +2,9 @@ import { Router } from "express"
import multer from "multer" import multer from "multer"
import { DeleteResponse } from "../../../../types/common" import { DeleteResponse } from "../../../../types/common"
import middlewares from "../../../middlewares" import middlewares, { transformBody } from "../../../middlewares"
import { AdminDeleteUploadsReq } from "./delete-upload"
import { AdminPostUploadsDownloadUrlReq } from "./get-download-url"
const route = Router() const route = Router()
const upload = multer({ dest: "uploads/" }) const upload = multer({ dest: "uploads/" })
@@ -16,21 +18,30 @@ export default (app) => {
middlewares.wrap(require("./create-upload").default) middlewares.wrap(require("./create-upload").default)
) )
route.delete("/", middlewares.wrap(require("./delete-upload").default)) route.delete(
"/",
transformBody(AdminDeleteUploadsReq),
middlewares.wrap(require("./delete-upload").default)
)
route.get( route.post(
"/download-url", "/download-url",
transformBody(AdminPostUploadsDownloadUrlReq),
middlewares.wrap(require("./get-download-url").default) middlewares.wrap(require("./get-download-url").default)
) )
return app return app
} }
export type AdminUploadRes = { export type AdminUploadsRes = {
uploads: unknown[] uploads: { url: string }[]
} }
export type AdminDeleteUploadRes = DeleteResponse export type AdminDeleteUploadsRes = DeleteResponse
export type AdminUploadsDownloadUrlRes = {
download_url: string
}
export * from "./create-upload" export * from "./create-upload"
export * from "./delete-upload" export * from "./delete-upload"