feat: order export and upload stream (#14243)

* feat: order export

* Merge branch 'develop' of https://github.com/medusajs/medusa into feat/order-export

* normalize status

* rm util

* serialize totals

* test

* lock

* comments

* configurable order list
This commit is contained in:
Carlos R. L. Rodrigues
2025-12-14 12:02:53 +01:00
committed by GitHub
parent e199f1eb01
commit 9366c6d468
31 changed files with 1041 additions and 37 deletions
@@ -49,7 +49,7 @@ describe.skip("S3 File Plugin", () => {
expect(resp).toEqual({
key: expect.stringMatching(/tests\/catphoto.*\.jpg/),
url: expect.stringMatching(/https:\/\/.*\.jpg/),
url: expect.stringMatching(/https?:\/\/.*\.jpg/),
})
const urlResp = await axios.get(resp.url).catch((e) => e.response)
@@ -95,7 +95,7 @@ describe.skip("S3 File Plugin", () => {
expect(resp).toEqual({
key: expect.stringMatching(/tests\/catphoto-か.*\.jpg/),
url: expect.stringMatching(/https:\/\/.*\/catphoto-%E3%81%8B.*\.jpg/),
url: expect.stringMatching(/https?:\/\/.*\/catphoto-%E3%81%8B.*\.jpg/),
})
})
@@ -112,7 +112,7 @@ describe.skip("S3 File Plugin", () => {
expect(resp).toEqual({
key: expect.stringMatching(/tests\/catphoto.*\.jpg/),
url: expect.stringMatching(/https:\/\/.*\/cat%3Fphoto.*\.jpg/),
url: expect.stringMatching(/https?:\/\/.*\/cat%3Fphoto.*\.jpg/),
})
})
@@ -128,7 +128,7 @@ describe.skip("S3 File Plugin", () => {
expect(resp).toEqual({
key: expect.stringMatching(/tests\/catphoto.*\.jpg/),
url: expect.stringMatching(/https:\/\/.*catphoto\.jpg/),
url: expect.stringMatching(/https?:\/\/.*catphoto\.jpg/),
})
const uploadResp = await axios.put(resp.url, fileContent, {
@@ -169,7 +169,7 @@ describe.skip("S3 File Plugin", () => {
expect(resp).toEqual({
key: expect.stringMatching(/tests\/testfolder\/catphoto.*\.jpg/),
url: expect.stringMatching(/https:\/\/.*testfolder\/catphoto\.jpg/),
url: expect.stringMatching(/https?:\/\/.*testfolder\/catphoto\.jpg/),
})
const uploadResp = await axios.put(resp.url, fileContent, {
@@ -221,4 +221,42 @@ describe.skip("S3 File Plugin", () => {
{ fileKey: cat2.key },
])
})
it("uploads using stream", async () => {
const fileContent = await fs.readFile(fixtureImagePath)
const fixtureAsBinary = fileContent.toString("binary")
const { writeStream, promise } = await s3Service.getUploadStream({
filename: "catphoto-stream.jpg",
mimeType: "image/jpeg",
access: "public",
})
writeStream.write(fileContent)
writeStream.end()
const resp = await promise
expect(resp).toEqual({
key: expect.stringMatching(/tests\/catphoto-stream.*\.jpg/),
url: expect.stringMatching(/https?:\/\/.*\.jpg/),
})
const urlResp = await axios.get(resp.url).catch((e) => e.response)
expect(urlResp.status).toEqual(200)
const signedUrl = await s3Service.getPresignedDownloadUrl({
fileKey: resp.key,
})
const signedUrlFile = Buffer.from(
await axios
.get(signedUrl, { responseType: "arraybuffer" })
.then((r) => r.data)
)
expect(signedUrlFile.toString("binary")).toEqual(fixtureAsBinary)
await s3Service.delete({ fileKey: resp.key })
})
})
@@ -30,6 +30,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.556.0",
"@aws-sdk/lib-storage": "^3.556.0",
"@aws-sdk/s3-request-presigner": "^3.556.0",
"ulid": "^2.3.0"
},
@@ -7,6 +7,7 @@ import {
S3Client,
S3ClientConfigType,
} from "@aws-sdk/client-s3"
import { Upload } from "@aws-sdk/lib-storage"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import {
FileTypes,
@@ -18,7 +19,7 @@ import {
MedusaError,
} from "@medusajs/framework/utils"
import path from "path"
import { Readable } from "stream"
import { PassThrough, Readable, Writable } from "stream"
import { ulid } from "ulid"
type InjectedDependencies = {
@@ -165,6 +166,53 @@ export class S3FileService extends AbstractFileProviderService {
}
}
async getUploadStream(fileData: FileTypes.ProviderUploadStreamDTO): Promise<{
writeStream: Writable
promise: Promise<FileTypes.ProviderFileResultDTO>
url: string
fileKey: string
}> {
if (!fileData.filename) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No filename provided`
)
}
const parsedFilename = path.parse(fileData.filename)
const fileKey = `${this.config_.prefix}${parsedFilename.name}-${ulid()}${
parsedFilename.ext
}`
const pass = new PassThrough()
const upload = new Upload({
client: this.client_,
params: {
ACL: fileData.access === "public" ? "public-read" : "private",
Bucket: this.config_.bucket,
Key: fileKey,
Body: pass,
ContentType: fileData.mimeType,
CacheControl: this.config_.cacheControl,
Metadata: {
"original-filename": encodeURIComponent(fileData.filename),
},
},
})
const promise = upload.done().then(() => ({
url: `${this.config_.fileUrl}/${fileKey}`,
key: fileKey,
}))
return {
writeStream: pass,
promise,
url: `${this.config_.fileUrl}/${fileKey}`,
fileKey,
}
}
async delete(
files: FileTypes.ProviderDeleteFileDTO | FileTypes.ProviderDeleteFileDTO[]
): Promise<void> {
@@ -207,7 +255,7 @@ export class S3FileService extends AbstractFileProviderService {
Key: `${fileData.fileKey}`,
})
return await getSignedUrl(this.client_, command, {
return await getSignedUrl(this.client_ as any, command as any, {
expiresIn: this.config_.downloadFileDuration,
})
}
@@ -238,7 +286,7 @@ export class S3FileService extends AbstractFileProviderService {
Key: fileKey,
})
const signedUrl = await getSignedUrl(this.client_, command, {
const signedUrl = await getSignedUrl(this.client_ as any, command as any, {
expiresIn:
fileData.expiresIn ?? DEFAULT_UPLOAD_EXPIRATION_DURATION_SECONDS,
})