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,5 @@
|
||||
Product Id,Product Handle,Product Title,Product Status,Product Description,Product Subtitle,Product External Id,Product Thumbnail,Product Collection Id,Product Type Id,Product Category 1,Product Created At,Product Deleted At,Product Discountable,Product Height,Product Hs Code,Product Image 1,Product Image 2,Product Is Giftcard,Product Length,Product Material,Product Mid Code,Product Origin Country,Product Tag 1,Product Tag 2,Product Updated At,Product Weight,Product Width,Variant Id,Variant Title,Variant Sku,Variant Upc,Variant Ean,Variant Hs Code,Variant Mid Code,Variant Manage Inventory,Variant Allow Backorder,Variant Barcode,Variant Created At,Variant Deleted At,Variant Height,Variant Length,Variant Material,Variant Metadata,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value,Variant Origin Country,Variant Price DKK,Variant Price EUR,Variant Price USD,Variant Product Id,Variant Updated At,Variant Variant Rank,Variant Weight,Variant Width,Shipping Profile Id
|
||||
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
||||
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRQ7WJ902X4936GEK7,Test variant,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,large,color,green,,30,45,100,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
||||
prod_01J44RRKH4HH2SANJ0S05YM853,base-product,Base product,draft,"test-product-description
|
||||
test line 2",,,test-image.png,pcol_01J44RRKG4P1RZ3CKAMBS760TD,ptyp_01J44RRKGHPQMJ1CRMW7MEN3P1,pcat_01J44RRKGS2N86A8V37ZJD877K,2024-07-31T16:07:55.681Z,,true,,,test-image.png,test-image-2.png,false,,,,,123,456,2024-07-31T16:07:55.681Z,,,variant_01J44RRKHRAYY7Q7NTXNNMDA1S,Test variant 2,,,,,,true,false,,2024-07-31T16:07:55.704Z,,,,,,size,small,color,green,,50,65,200,prod_01J44RRKH4HH2SANJ0S05YM853,2024-07-31T16:07:55.704Z,0,,,import-shipping-profile
|
||||
|
@@ -0,0 +1,84 @@
|
||||
import { join } from "path"
|
||||
import { readFile } from "fs/promises"
|
||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { AdminUploadPreSignedUrlRequest } from "@medusajs/types"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
const PRODUCTS_FILE_PATH = join(__dirname, "./__fixtures__", "products.csv")
|
||||
const getUploadReq = (file: File) => {
|
||||
return {
|
||||
body: {
|
||||
mime_type: file.type,
|
||||
originalname: file.name,
|
||||
size: file.size,
|
||||
access: "public",
|
||||
} satisfies AdminUploadPreSignedUrlRequest,
|
||||
meta: {
|
||||
headers: {
|
||||
...adminHeaders.headers,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
})
|
||||
|
||||
describe("POST /admin/uploads/presigned-urls", () => {
|
||||
it("should generate a signed URL to upload a file", async () => {
|
||||
const file = new File(
|
||||
[await readFile(PRODUCTS_FILE_PATH)],
|
||||
"products.csv",
|
||||
{
|
||||
type: "text/csv",
|
||||
}
|
||||
)
|
||||
const { body, meta } = getUploadReq(file)
|
||||
|
||||
const response = await api.post(
|
||||
"/admin/uploads/presigned-urls",
|
||||
body,
|
||||
meta
|
||||
)
|
||||
|
||||
expect(response.data).toEqual(
|
||||
expect.objectContaining({
|
||||
filename: expect.stringContaining(".csv"),
|
||||
extension: "csv",
|
||||
mime_type: "text/csv",
|
||||
size: file.size,
|
||||
url: expect.stringContaining(response.data.filename),
|
||||
})
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should return error when mime type is invalid", async () => {
|
||||
const file = new File(
|
||||
[await readFile(PRODUCTS_FILE_PATH)],
|
||||
"products.csv"
|
||||
)
|
||||
const { body, meta } = getUploadReq(file)
|
||||
|
||||
try {
|
||||
await api.post("/admin/uploads/presigned-urls", body, meta)
|
||||
} catch (error) {
|
||||
expect(error.response.data).toEqual({
|
||||
code: "invalid_data",
|
||||
type: "invalid_data",
|
||||
message: 'Invalid file type ""',
|
||||
})
|
||||
expect(error.response.status).toEqual(400)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -17,16 +17,16 @@ export class Product {
|
||||
/**
|
||||
* This method creates a product import. The products are only imported after
|
||||
* the import is confirmed using the {@link confirmImport} method.
|
||||
*
|
||||
* This method sends a request to the
|
||||
*
|
||||
* This method sends a request to the
|
||||
* [Create Product Import](https://docs.medusajs.com/api/admin#products_postproductsimport)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param body - The import's details.
|
||||
* @param query - Query parameters to pass to the request.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The import's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.import({
|
||||
* file // uploaded File instance
|
||||
@@ -58,17 +58,93 @@ export class Product {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates a product import. The products are only imported after
|
||||
* the import is confirmed using the {@link confirmImport} method.
|
||||
*
|
||||
* This method sends a request to the
|
||||
* [Create Product Import](https://docs.medusajs.com/api/admin#products_postproductsimport)
|
||||
* API route.
|
||||
*
|
||||
* @param body - The import's details.
|
||||
* @param query - Query parameters to pass to the request.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The import's details.
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.createImport({
|
||||
* file // uploaded File instance
|
||||
* })
|
||||
* .then(({ transaction_id }) => {
|
||||
* console.log(transaction_id)
|
||||
* })
|
||||
*/
|
||||
async createImport(
|
||||
body: HttpTypes.AdminImportProductRequest,
|
||||
query?: {},
|
||||
headers?: ClientHeaders
|
||||
) {
|
||||
/**
|
||||
* Get signed URL for file uploads
|
||||
*/
|
||||
const response =
|
||||
await this.client.fetch<HttpTypes.AdminUploadPreSignedUrlResponse>(
|
||||
"admin/uploads/presigned-urls",
|
||||
{
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: {
|
||||
originalname: body.file.name,
|
||||
mime_type: body.file.type,
|
||||
size: body.file.size,
|
||||
} satisfies HttpTypes.AdminUploadPreSignedUrlRequest,
|
||||
query,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Upload file using the signed URL. We cannot send cookies or any other
|
||||
* special headers in this request, since external services like S3 will
|
||||
* give a CORS error.
|
||||
*/
|
||||
await fetch(response.url, {
|
||||
method: "PUT",
|
||||
body: body.file,
|
||||
})
|
||||
|
||||
/**
|
||||
* Perform products import using the uploaded file name
|
||||
*/
|
||||
return await this.client.fetch<HttpTypes.AdminImportProductsResponse>(
|
||||
"/admin/products/imports",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
body: {
|
||||
file_key: response.filename,
|
||||
originalname: response.originalname,
|
||||
extension: response.extension,
|
||||
size: response.size,
|
||||
mime_type: response.mime_type,
|
||||
} satisfies HttpTypes.AdminImportProductsRequest,
|
||||
query,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method confirms a product import created using the method {@link import}.
|
||||
* It sends a request to the
|
||||
* It sends a request to the
|
||||
* [Confirm Product Import](https://docs.medusajs.com/api/admin#products_postproductsimporttransaction_idconfirm)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param transactionId - The ID of the transaction of the created product import. This is returned
|
||||
* by the API route used to create the product import.
|
||||
* @param query - Query parameters to pass in the request.
|
||||
* @param headers - Headers to pass in the request.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.confirmImport("transaction_123")
|
||||
* .then(() => {
|
||||
@@ -93,23 +169,23 @@ export class Product {
|
||||
|
||||
/**
|
||||
* This method starts a product export process to retrieve a CSV of exported products.
|
||||
*
|
||||
* You'll receive in the response the transaction ID of the workflow generating the CSV file.
|
||||
* To check the status of the execution, send a `GET` request to
|
||||
* `/admin/workflows-executions/export-products/:transaction-id`.
|
||||
*
|
||||
* Once the execution finishes successfully, a notification is created for the export.
|
||||
* You can retrieve the notifications using the `/admin/notification` API route to
|
||||
*
|
||||
* You'll receive in the response the transaction ID of the workflow generating the CSV file.
|
||||
* To check the status of the execution, send a `GET` request to
|
||||
* `/admin/workflows-executions/export-products/:transaction-id`.
|
||||
*
|
||||
* Once the execution finishes successfully, a notification is created for the export.
|
||||
* You can retrieve the notifications using the `/admin/notification` API route to
|
||||
* retrieve the file's download URL.
|
||||
*
|
||||
*
|
||||
* This method sends a request to the [Export Product](https://docs.medusajs.com/api/admin#products_postproductsexport)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param body - The export's details.
|
||||
* @param query - Filters to specify which products to export.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The export's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.export({})
|
||||
* .then(({ transaction_id }) => {
|
||||
@@ -133,15 +209,15 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method manages products to create, update, or delete them. It sends a request to the
|
||||
* This method manages products to create, update, or delete them. It sends a request to the
|
||||
* [Manage Products](https://docs.medusajs.com/api/admin#products_postproductsbatch)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param body - The products to create, update, or delete.
|
||||
* @param query - Configure the fields to retrieve in the products.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The batch operations details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.batch({
|
||||
* create: [
|
||||
@@ -189,15 +265,15 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates a product. It sends a request to the
|
||||
* This method creates a product. It sends a request to the
|
||||
* [Create Product](https://docs.medusajs.com/api/admin#products_postproducts)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param body - The product's details.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.create({
|
||||
* title: "Shirt",
|
||||
@@ -240,13 +316,13 @@ export class Product {
|
||||
* This method updates a product. It sends a request to the
|
||||
* [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param id - The product's ID.
|
||||
* @param body - The data to update in the product.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.update("prod_123", {
|
||||
* title: "Shirt",
|
||||
@@ -273,27 +349,27 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of products. It sends a request to the
|
||||
* This method retrieves a paginated list of products. It sends a request to the
|
||||
* [List Products](https://docs.medusajs.com/api/admin#products_getproducts) API route.
|
||||
*
|
||||
*
|
||||
* @param queryParams - Filters and pagination configurations.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The paginated list of products.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To retrieve the list of products:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.list()
|
||||
* .then(({ products, count, limit, offset }) => {
|
||||
* console.log(products)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To configure the pagination, pass the `limit` and `offset` query parameters.
|
||||
*
|
||||
*
|
||||
* For example, to retrieve only 10 items and skip 10 items:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.list({
|
||||
* limit: 10,
|
||||
@@ -303,10 +379,10 @@ export class Product {
|
||||
* console.log(products)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
|
||||
* in each products:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.list({
|
||||
* fields: "id,*variants"
|
||||
@@ -315,7 +391,7 @@ export class Product {
|
||||
* console.log(products)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async list(
|
||||
@@ -332,27 +408,27 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a product by its ID. It sends a request to the
|
||||
* This method retrieves a product by its ID. It sends a request to the
|
||||
* [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param id - The product's ID.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To retrieve a product by its ID:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.retrieve("prod_123")
|
||||
* .then(({ product }) => {
|
||||
* console.log(product)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify the fields and relations to retrieve:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.retrieve("prod_123", {
|
||||
* fields: "id,*variants"
|
||||
@@ -361,7 +437,7 @@ export class Product {
|
||||
* console.log(product)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async retrieve(id: string, query?: SelectParams, headers?: ClientHeaders) {
|
||||
@@ -378,11 +454,11 @@ export class Product {
|
||||
* This method deletes a product. It sends a request to the
|
||||
* [Delete Product](https://docs.medusajs.com/api/admin#products_deleteproductsid)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param id - The product's ID.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The deletion's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.delete("prod_123")
|
||||
* .then(({ deleted }) => {
|
||||
@@ -403,13 +479,13 @@ export class Product {
|
||||
* This method manages the variants of a product. It sends a request to the
|
||||
* [Manage Variants](https://docs.medusajs.com/api/admin#products_postproductsidvariantsbatch)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param body - The variants to create, update, or delete.
|
||||
* @param query - Configure the fields to retrieve in the product variants.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product variants' details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.batchVariants("prod_123", {
|
||||
* create: [
|
||||
@@ -454,13 +530,13 @@ export class Product {
|
||||
* This method creates a variant for a product. It sends a request to the
|
||||
* [Create Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param body - The variant's details.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.createVariant("prod_123", {
|
||||
* title: "Blue Shirt",
|
||||
@@ -499,17 +575,17 @@ export class Product {
|
||||
* This method updates a variant of a product. It sends a request to the
|
||||
* [Update Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param id - The variant's ID.
|
||||
* @param body - The data to update in the variant.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.updateVariant(
|
||||
* "prod_123",
|
||||
* "prod_123",
|
||||
* "variant_123",
|
||||
* {
|
||||
* title: "Blue Shirt",
|
||||
@@ -538,28 +614,28 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of products. It sends a request to the
|
||||
* This method retrieves a paginated list of products. It sends a request to the
|
||||
* [List Products](https://docs.medusajs.com/api/admin#products_getproductsidvariants) API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The ID of the product to retrieve its variants.
|
||||
* @param query - Filters and pagination configurations.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The paginated list of product variants.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To retrieve the list of product variants:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.listVariants("prod_123")
|
||||
* .then(({ variants, count, limit, offset }) => {
|
||||
* console.log(variants)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To configure the pagination, pass the `limit` and `offset` query parameters.
|
||||
*
|
||||
*
|
||||
* For example, to retrieve only 10 items and skip 10 items:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.listVariants("prod_123", {
|
||||
* limit: 10,
|
||||
@@ -569,10 +645,10 @@ export class Product {
|
||||
* console.log(variants)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
|
||||
* in each product variant:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.listVariants("prod_123", {
|
||||
* fields: "id,*product"
|
||||
@@ -581,7 +657,7 @@ export class Product {
|
||||
* console.log(variants)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async listVariants(
|
||||
@@ -602,16 +678,16 @@ export class Product {
|
||||
* This method retrieves a product's variant. It sends a request to the
|
||||
* [Retrieve Variant](https://docs.medusajs.com/api/admin#products_getproductsidvariantsvariant_id)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param id - The variant's ID.
|
||||
* @param query - Configure the fields to retrieve in the product variant.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product variant's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To retrieve a product variant by its ID:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.retrieveVariant(
|
||||
* "prod_123",
|
||||
@@ -621,9 +697,9 @@ export class Product {
|
||||
* console.log(variant)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify the fields and relations to retrieve:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.retrieveVariant(
|
||||
* "prod_123",
|
||||
@@ -636,7 +712,7 @@ export class Product {
|
||||
* console.log(variant)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async retrieveVariant(
|
||||
@@ -658,12 +734,12 @@ export class Product {
|
||||
* This method deletes a product's variant. It sends a request to the
|
||||
* [Delete Variant](https://docs.medusajs.com/api/admin#products_deleteproductsidvariantsvariant_id)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param id - The ID of the variant.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The deletion's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.deleteVariant("prod_123", "variant_123")
|
||||
* .then(({ deleted }) => {
|
||||
@@ -681,22 +757,22 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method manages a product's variant's inventories to associate them with inventory items,
|
||||
* This method manages a product's variant's inventories to associate them with inventory items,
|
||||
* update their inventory items, or delete their association with inventory items.
|
||||
*
|
||||
* It sends a request to the
|
||||
*
|
||||
* It sends a request to the
|
||||
* [Manage Variant Inventory](https://docs.medusajs.com/api/admin#products_postproductsidvariantsinventoryitemsbatch)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The ID of the product that the variant belongs to.
|
||||
* @param body - The inventory items to create, update, or delete.
|
||||
* @param query - Pass query parameters in the request.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The details of the created, updated, or deleted inventory items.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.batchVariantInventoryItems(
|
||||
* "prod_123",
|
||||
* "prod_123",
|
||||
* {
|
||||
* create: [
|
||||
* {
|
||||
@@ -745,16 +821,16 @@ export class Product {
|
||||
* This method creates an option in a product. It sends a request to the
|
||||
* [Create Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param body - The option's details.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.createOption(
|
||||
* "prod_123",
|
||||
* "prod_123",
|
||||
* {
|
||||
* title: "Color",
|
||||
* values: ["Green", "Blue"]
|
||||
@@ -785,17 +861,17 @@ export class Product {
|
||||
* This method updates a product's option. It sends a request to the
|
||||
* [Update Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param id - The ID of the option to update.
|
||||
* @param body - The data to update in the option.
|
||||
* @param query - Configure the fields to retrieve in the product.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.updateOption(
|
||||
* "prod_123",
|
||||
* "prod_123",
|
||||
* "prodopt_123",
|
||||
* {
|
||||
* title: "Color"
|
||||
@@ -824,28 +900,28 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of product options. It sends a request to the
|
||||
* This method retrieves a paginated list of product options. It sends a request to the
|
||||
* [List Options](https://docs.medusajs.com/api/admin#products_getproductsidoptions) API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The ID of the product to retrieve its options
|
||||
* @param query - Filters and pagination configurations.
|
||||
* @param headers - Headers to pass in the request.
|
||||
* @returns The paginated list of product options.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To retrieve the list of product options:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.listOptions("prod_123")
|
||||
* .then(({ product_options, count, limit, offset }) => {
|
||||
* console.log(product_options)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To configure the pagination, pass the `limit` and `offset` query parameters.
|
||||
*
|
||||
*
|
||||
* For example, to retrieve only 10 items and skip 10 items:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.listOptions("prod_123", {
|
||||
* limit: 10,
|
||||
@@ -855,10 +931,10 @@ export class Product {
|
||||
* console.log(product_options)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
|
||||
* in each product options:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.listOptions("prod_123", {
|
||||
* fields: "id,title"
|
||||
@@ -867,7 +943,7 @@ export class Product {
|
||||
* console.log(product_options)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async listOptions(
|
||||
@@ -888,16 +964,16 @@ export class Product {
|
||||
* This method retrieves a product's option. It sends a request to the
|
||||
* [Get Option](https://docs.medusajs.com/api/admin#products_getproductsidoptionsoption_id)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param id - The product option's ID.
|
||||
* @param query - Configure the fields to retrieve in the product option.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The product option's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To retrieve a product option by its ID:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.retrieveOption(
|
||||
* "prod_123",
|
||||
@@ -907,9 +983,9 @@ export class Product {
|
||||
* console.log(product_option)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* To specify the fields and relations to retrieve:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* sdk.admin.product.retrieveOption(
|
||||
* "prod_123",
|
||||
@@ -922,7 +998,7 @@ export class Product {
|
||||
* console.log(product_option)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
|
||||
*/
|
||||
async retrieveOption(
|
||||
@@ -941,15 +1017,15 @@ export class Product {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deletes a product's option. It sends a request to the
|
||||
* This method deletes a product's option. It sends a request to the
|
||||
* [Delete Option](https://docs.medusajs.com/api/admin#products_deleteproductsidoptionsoption_id)
|
||||
* API route.
|
||||
*
|
||||
*
|
||||
* @param productId - The product's ID.
|
||||
* @param id - The option's ID.
|
||||
* @param headers - Headers to pass in the request
|
||||
* @returns The deletion's details.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* sdk.admin.product.deleteOption("prod_123", "prodopt_123")
|
||||
* .then(({ deleted }) => {
|
||||
|
||||
@@ -13,7 +13,12 @@ export type Config = {
|
||||
auth?: {
|
||||
type?: "jwt" | "session"
|
||||
jwtTokenStorageKey?: string
|
||||
jwtTokenStorageMethod?: "local" | "session" | "memory" | "custom" | "nostore"
|
||||
jwtTokenStorageMethod?:
|
||||
| "local"
|
||||
| "session"
|
||||
| "memory"
|
||||
| "custom"
|
||||
| "nostore"
|
||||
fetchCredentials?: "include" | "omit" | "same-origin"
|
||||
storage?: CustomStorage
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ export interface IFileProvider {
|
||||
/**
|
||||
* Get the file contents as a readable stream.
|
||||
*/
|
||||
getAsStream(fileData: ProviderGetFileDTO): Promise<Readable>
|
||||
getDownloadStream(fileData: ProviderGetFileDTO): Promise<Readable>
|
||||
|
||||
/**
|
||||
* Get the file contents as a Node.js Buffer
|
||||
|
||||
@@ -169,10 +169,10 @@ export interface IFileModuleService extends IModuleService {
|
||||
* Get the file contents as a readable stream.
|
||||
*
|
||||
* @example
|
||||
* const stream = await fileModuleService.getAsStream("file_123")
|
||||
* const stream = await fileModuleService.getDownloadStream("file_123")
|
||||
* writeable.pipe(stream)
|
||||
*/
|
||||
getAsStream(id: string, sharedContext?: Context): Promise<Readable>
|
||||
getDownloadStream(id: string, sharedContext?: Context): Promise<Readable>
|
||||
|
||||
/**
|
||||
* Get the file contents as a Node.js Buffer
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { BaseUploadFile } from "../common"
|
||||
|
||||
export type AdminUploadFile = BaseUploadFile
|
||||
|
||||
export interface AdminUploadPreSignedUrlRequest {
|
||||
originalname: string
|
||||
mime_type: string
|
||||
size: number
|
||||
access?: "public" | "private"
|
||||
}
|
||||
|
||||
@@ -16,3 +16,36 @@ export interface AdminFileListResponse {
|
||||
}
|
||||
|
||||
export type AdminFileDeleteResponse = DeleteResponse<"file">
|
||||
|
||||
export interface AdminUploadPreSignedUrlResponse {
|
||||
/**
|
||||
* The URL to be used for uploading the file
|
||||
*/
|
||||
url: string
|
||||
|
||||
/**
|
||||
* The unique filename that can be used with the file provider
|
||||
* to fetch the file
|
||||
*/
|
||||
filename: string
|
||||
|
||||
/**
|
||||
* The original name of the file on the user's computer (aka clientName)
|
||||
*/
|
||||
originalname: string
|
||||
|
||||
/**
|
||||
* The mime type of the file.
|
||||
*/
|
||||
mime_type: string
|
||||
|
||||
/**
|
||||
* Extension of the file to be uploaded
|
||||
*/
|
||||
extension: string
|
||||
|
||||
/**
|
||||
* Size of the file to be uploaded (in bytes)
|
||||
*/
|
||||
size: number
|
||||
}
|
||||
|
||||
@@ -570,3 +570,11 @@ interface AdminDeleteProductVariantInventoryItem {
|
||||
*/
|
||||
variant_id: string
|
||||
}
|
||||
|
||||
export interface AdminImportProductsRequest {
|
||||
file_key: string
|
||||
originalname: string
|
||||
extension: string
|
||||
size: number
|
||||
mime_type: string
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface AdminExportProductResponse {
|
||||
|
||||
export interface AdminImportProductResponse {
|
||||
/**
|
||||
* The ID of the import product workflow execution's transaction.
|
||||
* The ID of the import product workflow execution's transaction.
|
||||
* This is useful to confirm the import using the `/admin/products/:transaction-id/import` API route.
|
||||
*/
|
||||
transaction_id: string
|
||||
@@ -73,6 +73,9 @@ export interface AdminImportProductResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminImportProductsResponse
|
||||
extends AdminImportProductResponse {}
|
||||
|
||||
export interface AdminBatchProductVariantResponse
|
||||
extends BatchResponse<AdminProductVariant> {}
|
||||
|
||||
|
||||
@@ -187,8 +187,8 @@ export class AbstractFileProviderService implements IFileProvider {
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
getAsStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
throw Error("getAsStream must be overridden by the child class")
|
||||
getDownloadStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
throw Error("getDownloadStream must be overridden by the child class")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({})
|
||||
}
|
||||
27
packages/medusa/src/api/admin/products/imports/route.ts
Normal file
27
packages/medusa/src/api/admin/products/imports/route.ts
Normal file
@@ -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
|
||||
>
|
||||
|
||||
@@ -167,8 +167,8 @@ export default class FileModuleService implements FileTypes.IFileModuleService {
|
||||
* const stream = await fileModuleService.getAsStream("file_123")
|
||||
* writeable.pipe(stream)
|
||||
*/
|
||||
getAsStream(id: string): Promise<Readable> {
|
||||
return this.fileProviderService_.getAsStream({ fileKey: id })
|
||||
getDownloadStream(id: string): Promise<Readable> {
|
||||
return this.fileProviderService_.getDownloadStream({ fileKey: id })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,8 +70,8 @@ export default class FileProviderService {
|
||||
return this.fileProvider_.getPresignedUploadUrl(fileData)
|
||||
}
|
||||
|
||||
getAsStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
return this.fileProvider_.getAsStream(fileData)
|
||||
getDownloadStream(fileData: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
return this.fileProvider_.getDownloadStream(fileData)
|
||||
}
|
||||
|
||||
getAsBuffer(fileData: FileTypes.ProviderGetFileDTO): Promise<Buffer> {
|
||||
|
||||
@@ -85,7 +85,9 @@ export class LocalFileService extends AbstractFileProviderService {
|
||||
return
|
||||
}
|
||||
|
||||
async getAsStream(file: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
async getDownloadStream(
|
||||
file: FileTypes.ProviderGetFileDTO
|
||||
): Promise<Readable> {
|
||||
const baseDir = file.fileKey.startsWith("private-")
|
||||
? this.privateUploadDir_
|
||||
: this.uploadDir_
|
||||
@@ -124,6 +126,47 @@ export class LocalFileService extends AbstractFileProviderService {
|
||||
return this.getUploadFileUrl(file.fileKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pre-signed URL that the client (frontend) can use to trigger
|
||||
* a file upload. In this case, the Medusa backend will implement the
|
||||
* "/upload" endpoint to perform the file upload.
|
||||
*
|
||||
* Since, we do not want the client to perform link detection on the frontend
|
||||
* and then prepare a different kind of request for cloud providers and different
|
||||
* request for the local server, we will have to make these URLs self sufficient.
|
||||
*
|
||||
* What is a self sufficient URL
|
||||
*
|
||||
* - There should be no need to specify the MIME type or filename separately in request body (cloud providers don't allow it).
|
||||
* - There should be no need to pass auth headers like cookies. Again cloud providers
|
||||
* won't allow it and will likely result in a CORS error.
|
||||
*/
|
||||
async getPresignedUploadUrl(
|
||||
fileData: FileTypes.ProviderGetPresignedUploadUrlDTO
|
||||
): Promise<FileTypes.ProviderFileResultDTO> {
|
||||
if (!fileData?.filename) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`No filename provided`
|
||||
)
|
||||
}
|
||||
|
||||
const uploadUrl = new URL(
|
||||
"upload",
|
||||
`${this.backendUrl_.replace(/\/$/, "")}/`
|
||||
)
|
||||
|
||||
uploadUrl.searchParams.set("filename", fileData.filename)
|
||||
if (fileData.mimeType) {
|
||||
uploadUrl.searchParams.set("type", fileData.mimeType)
|
||||
}
|
||||
|
||||
return {
|
||||
url: uploadUrl.toString(),
|
||||
key: fileData.filename,
|
||||
}
|
||||
}
|
||||
|
||||
private getUploadFilePath = (baseDir: string, fileKey: string) => {
|
||||
return path.join(baseDir, fileKey)
|
||||
}
|
||||
|
||||
@@ -217,15 +217,17 @@ export class S3FileService extends AbstractFileProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAsStream(file: FileTypes.ProviderGetFileDTO): Promise<Readable> {
|
||||
if (!file?.filename) {
|
||||
async getDownloadStream(
|
||||
file: FileTypes.ProviderGetFileDTO
|
||||
): Promise<Readable> {
|
||||
if (!file?.fileKey) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`No filename provided`
|
||||
`No fileKey provided`
|
||||
)
|
||||
}
|
||||
|
||||
const fileKey = `${this.config_.prefix}${file.filename}`
|
||||
const fileKey = `${this.config_.prefix}${file.fileKey}`
|
||||
const response = await this.client_.send(
|
||||
new GetObjectCommand({
|
||||
Key: fileKey,
|
||||
@@ -237,14 +239,14 @@ export class S3FileService extends AbstractFileProviderService {
|
||||
}
|
||||
|
||||
async getAsBuffer(file: FileTypes.ProviderGetFileDTO): Promise<Buffer> {
|
||||
if (!file?.filename) {
|
||||
if (!file?.fileKey) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`No filename provided`
|
||||
`No fileKey provided`
|
||||
)
|
||||
}
|
||||
|
||||
const fileKey = `${this.config_.prefix}${file.filename}`
|
||||
const fileKey = `${this.config_.prefix}${file.fileKey}`
|
||||
const response = await this.client_.send(
|
||||
new GetObjectCommand({
|
||||
Key: fileKey,
|
||||
|
||||
Reference in New Issue
Block a user