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:
Harminder Virk authored and GitHub committed 2025-05-04 12:58:10 +02:00
1 parent ed55d17a36
commit 84f58f9058
22 files changed
+537 -129

No files matched your search

+177 -101
View File
@@ -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 }) => {
+6 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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")
}
/**