fix(medusa,js-sdk,types): Add basic draft order operations to js-sdk (#11514)

**What**
- Exposes `sdk.admin.draftOrder.create/update/retrieve/list` functions from the js-sdk
- Implements the necessary types in the types package.
- Adds missing endpoints to admin API.
This commit is contained in:
Kasper Fabricius Kristensen
2025-02-20 17:05:21 +01:00
committed by GitHub
parent 120e6f9ba6
commit 3b4997840e
17 changed files with 614 additions and 54 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/js-sdk": patch
"@medusajs/types": patch
"@medusajs/medusa": patch
---
fix(medusa,js-sdk,types): Add basic draft order operations to js-sdk

View File

@@ -0,0 +1,106 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { HttpTypes } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils"
import {
adminHeaders,
createAdminUser,
} from "../../../../helpers/create-admin-user"
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
jest.setTimeout(300000)
medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let region: HttpTypes.AdminRegion
let testDraftOrder: HttpTypes.AdminDraftOrder
beforeEach(async () => {
const container = getContainer()
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
await createAdminUser(dbConnection, adminHeaders, container)
region = (
await api.post(
`/admin/regions`,
{
name: "USA",
currency_code: "usd",
countries: ["US"],
},
adminHeaders
)
).data.region
testDraftOrder = (
await api.post(
"/admin/draft-orders",
{ email: "test@test.com", region_id: region.id },
adminHeaders
)
).data.draft_order
})
describe("GET /draft-orders", () => {
it("should get a list of draft orders", async () => {
const response = await api.get("/admin/draft-orders", adminHeaders)
expect(response.status).toBe(200)
expect(response.data.draft_orders).toBeDefined()
expect(response.data.draft_orders.length).toBe(1)
expect(response.data.draft_orders).toEqual(
expect.arrayContaining([
expect.objectContaining({
email: "test@test.com",
}),
])
)
})
})
describe("POST /draft-orders", () => {
it("should create a draft order", async () => {
const response = await api.post(
"/admin/draft-orders",
{
email: "test2@test.com",
region_id: region.id,
},
adminHeaders
)
expect(response.status).toBe(200)
expect(response.data.draft_order.email).toBe("test2@test.com")
expect(response.data.draft_order.region_id).toBe(region.id)
})
})
describe("GET /draft-orders/:id", () => {
it("should get a draft order", async () => {
const response = await api.get(
`/admin/draft-orders/${testDraftOrder.id}`,
adminHeaders
)
expect(response.status).toBe(200)
expect(response.data.draft_order.email).toBe("test@test.com")
expect(response.data.draft_order.region_id).toBe(region.id)
})
})
describe("POST /draft-orders/:id", () => {
it("should update a draft order", async () => {
const response = await api.post(
`/admin/draft-orders/${testDraftOrder.id}`,
{
email: "test_new@test.com",
},
adminHeaders
)
expect(response.status).toBe(200)
expect(response.data.draft_order.email).toBe("test_new@test.com")
})
})
},
})

View File

@@ -0,0 +1,205 @@
import { HttpTypes } from "@medusajs/types"
import { Client } from "../client"
import { ClientHeaders } from "../types"
export class DraftOrder {
/**
* @ignore
*/
private client: Client
/**
* @ignore
*/
constructor(client: Client) {
this.client = client
}
/**
* This method retrieves a draft order by its ID. It sends a request to the
* [Get Draft Order](https://docs.medusajs.com/api/admin#draft-orders_getdraftordersid)
* API route.
*
* @param id - The draft order's ID.
* @param query - Configure the fields to retrieve in the draft order.
* @param headers - Headers to pass in the request
* @returns The draft order's details.
*
* @example
* To retrieve a draft order by its ID:
*
* ```ts
* sdk.admin.draftOrder.retrieve("draft_order_123")
* .then(({ draft_order }) => {
* console.log(draft_order)
* })
* ```
*
* To specify the fields and relations to retrieve:
*
* ```ts
* sdk.admin.draftOrder.retrieve("draft_order_123", {
* fields: "id,*items"
* })
* .then(({ draft_order }) => {
* console.log(draft_order)
* })
* ```
*
* 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?: HttpTypes.AdminDraftOrderParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminDraftOrderResponse>(
`/admin/draft-orders/${id}`,
{
query,
headers,
}
)
}
/**
* This method retrieves a paginated list of draft orders. It sends a request to the
* [List Draft Orders](https://docs.medusajs.com/api/admin#draft-orders_getdraftorders) API route.
*
* @param queryParams - Filters and pagination configurations.
* @param headers - Headers to pass in the request.
* @returns The paginated list of draft orders.
*
* @example
* To retrieve the list of draft orders:
*
* ```ts
* sdk.admin.draftOrder.list()
* .then(({ draft_orders, count, limit, offset }) => {
* console.log(draft_orders)
* })
* ```
*
* 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.draftOrder.list({
* limit: 10,
* offset: 10
* })
* .then(({ draft_orders, count, limit, offset }) => {
* console.log(draft_orders)
* })
* ```
*
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
* in each draft order:
*
* ```ts
* sdk.admin.draftOrder.list({
* fields: "id,*items"
* })
* .then(({ draft_orders, count, limit, offset }) => {
* console.log(draft_orders)
* })
* ```
*
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
*/
async list(
queryParams?: HttpTypes.AdminDraftOrderListParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminDraftOrderListResponse>(
`/admin/draft-orders`,
{
query: queryParams,
headers,
}
)
}
/**
* This method creates a draft order. It sends a request to the
* [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders) API route.
*
* @param body - The data to create the draft order.
* @param query - Configure the fields to retrieve in the draft order.
* @param headers - Headers to pass in the request.
*
* @example
* To create a draft order:
*
* ```ts
* sdk.admin.draftOrder.create({
* email: "test@test.com",
* items: [
* {
* variant_id: "variant_123",
* quantity: 1,
* },
* ],
* region_id: "region_123",
* sales_channel_id: "sales_channel_123",
* })
* .then(({ draft_order }) => {
* console.log(draft_order)
* })
* ```
*/
async create(
body: HttpTypes.AdminCreateDraftOrder,
query?: HttpTypes.AdminDraftOrderParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminDraftOrderResponse>(
`/admin/draft-orders`,
{
method: "POST",
body,
query,
headers,
}
)
}
/**
* This method updates a draft order. It sends a request to the
* [Update Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersid) API route.
*
* @param id - The draft order's ID.
* @param body - The data to update the draft order.
* @param query - Configure the fields to retrieve in the draft order.
* @param headers - Headers to pass in the request.
*
* @example
* To update a draft order:
*
* ```ts
* sdk.admin.draftOrder.update("draft_order_123", {
* email: "test@test.com",
* })
* .then(({ draft_order }) => {
* console.log(draft_order)
* })
* ```
*/
async update(
id: string,
body: HttpTypes.AdminUpdateDraftOrder,
query?: HttpTypes.AdminDraftOrderParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminDraftOrderResponse>(
`/admin/draft-orders/${id}`,
{
method: "POST",
body,
query,
headers,
}
)
}
}

View File

@@ -5,6 +5,7 @@ import { Claim } from "./claim"
import { Currency } from "./currency"
import { Customer } from "./customer"
import { CustomerGroup } from "./customer-group"
import { DraftOrder } from "./draft-order"
import { Exchange } from "./exchange"
import { Fulfillment } from "./fulfillment"
import { FulfillmentProvider } from "./fulfillment-provider"
@@ -126,6 +127,10 @@ export class Admin {
* @tags order
*/
public order: Order
/**
* @tags draft order
*/
public draftOrder: DraftOrder
/**
* @tags order
*/

View File

@@ -0,0 +1,3 @@
import { AdminOrder } from "../../order"
export interface AdminDraftOrder extends AdminOrder {}

View File

@@ -0,0 +1,4 @@
export * from "./entities"
export * from "./payloads"
export * from "./queries"
export * from "./responses"

View File

@@ -0,0 +1,111 @@
import { OrderAddress } from "../../order"
export interface AdminCreateDraftOrderItem {
/**
* The item's title.
*/
title?: string | null
/**
* The item's variant SKU.
*/
variant_sku?: string | null
/**
* The item's variant barcode.
*/
variant_barcode?: string | null
/**
* The ID of the item's variant.
*/
variant_id?: string | null
/**
* The item's unit price.
*/
unit_price?: number | null
/**
* The item's quantity.
*/
quantity: number
/**
* The item's metadata.
*/
metadata?: Record<string, unknown> | null
}
export interface AdminCreateDraftOrderShippingMethod {
/**
* The ID of the shipping option.
*/
shipping_option_id: string
}
export interface AdminCreateDraftOrder {
/**
* The draft order's email.
*
* Either email or customer_id must be provided.
*/
email?: string | null
/**
* The ID of the customer to associate the draft order with.
*
* Either customer_id or email must be provided.
*/
customer_id?: string | null
/**
* The ID of the region to associate the draft order with.
*/
region_id: string
/**
* The currency code to use for the draft order.
*
* If not provided, the currency from the region will be used.
*/
currency_code?: string | null
/**
* The promotions to apply to the draft order.
*/
promo_codes?: string[]
/**
* The draft order's shipping address.
*/
shipping_address?: OrderAddress | string
/**
* The draft order's billing address.
*/
billing_address?: OrderAddress | string
/**
* The draft order's items.
*/
items?: AdminCreateDraftOrderItem[]
/**
* The draft order's shipping methods.
*/
shipping_methods?: AdminCreateDraftOrderShippingMethod[]
/**
* Whether to notify the customer about the draft order.
*/
no_notification_order?: boolean
/**
* The draft order's metadata.
*/
metadata?: Record<string, unknown> | null
}
export interface AdminUpdateDraftOrder {
/**
* The draft order's email.
*/
email?: string
/**
* The draft order's shipping address.
*/
shipping_address?: OrderAddress
/**
* The draft order's billing address.
*/
billing_address?: OrderAddress
/**
* The draft order's metadata.
*/
metadata?: Record<string, unknown> | null
}

View File

@@ -0,0 +1,37 @@
import { BaseFilterable, OperatorMap } from "../../../dal"
import { FindParams, SelectParams } from "../../common"
export interface AdminDraftOrderParams extends SelectParams {}
export interface AdminDraftOrderListParams
extends FindParams,
BaseFilterable<AdminDraftOrderListParams> {
/**
* Filter by draft order ID(s).
*/
id?: string | string[]
/**
* Query or keywords to filter the draft order's searchable fields.
*/
q?: string
/**
* Filter by region IDs to retrieve their associated draft orders.
*/
region_id?: string[] | string
/**
* Filter by customer IDs to retrieve their associated draft orders.
*/
customer_id?: string[] | string
/**
* Filter by sales channel IDs to retrieve their associated draft orders.
*/
sales_channel_id?: string[]
/**
* Apply filters on the draft order's creation date.
*/
created_at?: OperatorMap<string>
/**
* Apply filters on the draft order's update date.
*/
updated_at?: OperatorMap<string>
}

View File

@@ -0,0 +1,11 @@
import { PaginatedResponse } from "../../common"
import { AdminDraftOrder } from "./entities"
export interface AdminDraftOrderResponse {
draft_order: AdminDraftOrder
}
export interface AdminDraftOrderListResponse
extends PaginatedResponse<{
draft_orders: AdminDraftOrder[]
}> {}

View File

@@ -0,0 +1 @@
export * from "./admin"

View File

@@ -9,6 +9,7 @@ export * from "./common"
export * from "./currency"
export * from "./customer"
export * from "./customer-group"
export * from "./draft-order"
export * from "./exchange"
export * from "./file"
export * from "./fulfillment"

View File

@@ -40,11 +40,3 @@ export interface AdminOrderPreviewResponse {
*/
order: AdminOrderPreview
}
export interface AdminDraftOrderResponse {
draft_order: AdminOrder
}
export type AdminDraftOrderListResponse = PaginatedResponse<{
draft_orders: AdminOrder[]
}>

View File

@@ -1,25 +1,56 @@
import { MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { refetchOrder } from "../helpers"
import { defaultAdminOrderFields } from "../query-config"
import {
getOrderDetailWorkflow,
updateOrderWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { HttpTypes } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { AdminUpdateDraftOrderType } from "../validators"
export const GET = async (
req: MedusaRequest,
res: MedusaResponse<HttpTypes.AdminDraftOrderResponse>
) => {
const draftOrder = await refetchOrder(
req.params.id,
req.scope,
defaultAdminOrderFields
)
const workflow = getOrderDetailWorkflow(req.scope)
const { result } = await workflow.run({
input: {
fields: req.queryConfig.fields,
order_id: req.params.id,
version: req.validatedQuery.version as number,
filters: {
is_draft_order: true,
},
},
})
if (!draftOrder) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Draft order with id: ${req.params.id} was not found`
)
}
res.status(200).json({ draft_order: draftOrder })
res.status(200).json({ draft_order: result as HttpTypes.AdminDraftOrder })
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateDraftOrderType>,
res: MedusaResponse<HttpTypes.AdminDraftOrderResponse>
) => {
const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
await updateOrderWorkflow(req.scope).run({
input: {
...req.validatedBody,
user_id: req.auth_context.actor_id,
id: req.params.id,
},
})
const result = await query.graph({
entity: "order",
filters: { id: req.params.id },
fields: req.queryConfig.fields,
})
res
.status(200)
.json({ draft_order: result.data[0] as HttpTypes.AdminDraftOrder })
}

View File

@@ -1,11 +1,14 @@
import {
validateAndTransformBody,
validateAndTransformQuery,
} from "@medusajs/framework"
import { MiddlewareRoute } from "@medusajs/framework/http"
import { validateAndTransformBody } from "@medusajs/framework"
import { validateAndTransformQuery } from "@medusajs/framework"
import * as QueryConfig from "./query-config"
import {
AdminCreateDraftOrder,
AdminGetOrderParams,
AdminGetOrdersParams,
AdminGetDraftOrderParams,
AdminGetDraftOrdersParams,
AdminUpdateDraftOrder,
} from "./validators"
export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [
@@ -14,7 +17,7 @@ export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/draft-orders",
middlewares: [
validateAndTransformQuery(
AdminGetOrdersParams,
AdminGetDraftOrdersParams,
QueryConfig.listTransformQueryConfig
),
],
@@ -24,7 +27,7 @@ export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/draft-orders/:id",
middlewares: [
validateAndTransformQuery(
AdminGetOrderParams,
AdminGetDraftOrderParams,
QueryConfig.retrieveTransformQueryConfig
),
],
@@ -35,7 +38,18 @@ export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [
validateAndTransformBody(AdminCreateDraftOrder),
validateAndTransformQuery(
AdminGetOrderParams,
AdminGetDraftOrderParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/draft-orders/:id",
middlewares: [
validateAndTransformBody(AdminUpdateDraftOrder),
validateAndTransformQuery(
AdminGetDraftOrderParams,
QueryConfig.retrieveTransformQueryConfig
),
],

View File

@@ -1,7 +1,10 @@
export const defaultAdminListOrderFields = [
"id",
"display_id",
"status",
"version",
"email",
"region_id",
"*items",
"summary",
"metadata",
@@ -11,8 +14,11 @@ export const defaultAdminListOrderFields = [
export const defaultAdminOrderFields = [
"id",
"display_id",
"status",
"version",
"email",
"region_id",
"*items",
"*items.tax_lines",
"*items.adjustments",

View File

@@ -1,4 +1,7 @@
import { createOrderWorkflow } from "@medusajs/core-flows"
import {
createOrderWorkflow,
getOrdersListWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaRequest,
@@ -8,6 +11,7 @@ import {
AdditionalData,
CreateOrderDTO,
HttpTypes,
OrderDTO,
} from "@medusajs/framework/types"
import {
ContainerRegistrationKeys,
@@ -21,24 +25,28 @@ export const GET = async (
req: MedusaRequest<HttpTypes.AdminOrderFilters>,
res: MedusaResponse<HttpTypes.AdminDraftOrderListResponse>
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "order",
variables: {
filters: {
...req.filterableFields,
is_draft_order: true,
},
...req.queryConfig.pagination,
const variables = {
filters: {
...req.filterableFields,
is_draft_order: true,
},
...req.queryConfig.pagination,
}
const workflow = getOrdersListWorkflow(req.scope)
const { result } = await workflow.run({
input: {
fields: req.queryConfig.fields,
variables,
},
fields: req.queryConfig.fields,
})
const { rows: draft_orders, metadata } = await remoteQuery(queryObject)
const { rows, metadata } = result as {
rows: OrderDTO[]
metadata: any
}
res.json({
draft_orders,
draft_orders: rows as unknown as HttpTypes.AdminOrder[],
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,

View File

@@ -6,24 +6,34 @@ import {
} from "../../utils/common-validators"
import {
createFindParams,
createOperatorMap,
createSelectParams,
WithAdditionalData,
} from "../../utils/validators"
export type AdminGetOrderParamsType = z.infer<typeof AdminGetOrderParams>
export const AdminGetOrderParams = createSelectParams()
export type AdminGetDraftOrderParamsType = z.infer<
typeof AdminGetDraftOrderParams
>
export const AdminGetDraftOrderParams = createSelectParams()
export const AdminGetOrdersParamsFields = z.object({
const AdminGetDraftOrdersParamsFields = z.object({
id: z.union([z.string(), z.array(z.string())]).optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
q: z.string().optional(),
region_id: z.union([z.string(), z.array(z.string())]).optional(),
sales_channel_id: z.array(z.string()).optional(),
})
export type AdminGetOrdersParamsType = z.infer<typeof AdminGetOrdersParams>
export const AdminGetOrdersParams = createFindParams({
export type AdminGetDraftOrdersParamsType = z.infer<
typeof AdminGetDraftOrdersParams
>
export const AdminGetDraftOrdersParams = createFindParams({
limit: 50,
offset: 0,
})
.merge(AdminGetOrdersParamsFields)
.merge(applyAndAndOrOperators(AdminGetOrdersParamsFields))
.merge(AdminGetDraftOrdersParamsFields)
.merge(applyAndAndOrOperators(AdminGetDraftOrdersParamsFields))
enum Status {
completed = "completed",
@@ -91,3 +101,11 @@ export const AdminCreateDraftOrder = WithAdditionalData(
)
}
)
export type AdminUpdateDraftOrderType = z.infer<typeof AdminUpdateDraftOrder>
export const AdminUpdateDraftOrder = z.object({
email: z.string().optional(),
shipping_address: AddressPayload.optional(),
billing_address: AddressPayload.optional(),
metadata: z.record(z.unknown()).nullish(),
})