Skip to content

fix(framework,medusa): reject sorting admin orders by computed fields with a 400#16070

Open
hhuang2088 wants to merge 3 commits into
medusajs:developfrom
hhuang2088:fix/order_sort_deny_list
Open

fix(framework,medusa): reject sorting admin orders by computed fields with a 400#16070
hhuang2088 wants to merge 3 commits into
medusajs:developfrom
hhuang2088:fix/order_sort_deny_list

Conversation

@hhuang2088

Copy link
Copy Markdown

What

Sorting the admin orders list (GET /admin/orders) by a computed field —
any total (total, subtotal, tax_total, item_*, shipping_*,
original_*, credit_line_*, …), payment_status, or fulfillment_status
currently throws a 500, because these are not persisted columns on the order
table (MikroORM: "Trying to order by not existing property Order.total").
This PR rejects sorting by these fields with a clean 400.

Why

The orders list query config never guarded the order query param, so it
flowed unchecked to the data layer. This mirrors the filtering fix in #15262
(nonFilterableFields), but for sorting — the counterpart was missing.

Per review feedback, the guard is a deny list rather than an allow list:
an allow list would 400 real columns (email, status, currency_code, …)
that sorted fine before — a breaking change. Every field in the deny list
previously threw a 500, so forbidding them changes no working request.

How

  • Added an optional forbiddenOrderBy to QueryConfig (@medusajs/types).
    Unlike allowed, it governs only sorting, not field selection — so a
    computed field like total stays selectable while being rejected as a sort
    key.
  • Enforced it in prepareListQuery (@medusajs/framework), alongside the
    untouched pre-existing allowed check — no other route changes behavior.
  • Defined forbiddenAdminOrderSortFields on the orders list config: the
    computed totals (mirroring shouldIncludeTotals in the order module
    service), payment_status, fulfillment_status, and summary.

Testing

  • Unit (packages/core/framework/src/http/utils/__tests__/get-query-config.spec.ts):
    added cases for prepareListQuery — rejecting a sort field in
    forbiddenOrderBy (ascending and descending), allowing one outside it,
    rejecting a forbidden field even when it is in allowed (sort decoupled
    from field selection), and still enforcing allowed when forbiddenOrderBy
    is set.
  • Integration (integration-tests/http/__tests__/order/admin/order.spec.ts):
    added cases under GET /admin/orders — 400 when sorting by total,
    -payment_status, fulfillment_status, subtotal, -shipping_total,
    credit_line_tax_total, or original_item_subtotal; 200 when sorting by
    real columns -created_at, display_id, email, and status (including
    ones outside any allow list, proving no regression for existing sorts).
    The pre-existing fields=id,total&order=-created_at test still passes,
    proving field selection is unaffected.

Supersedes #15809 — same change reworked per review feedback there (allow
list → deny list), rebased onto the latest develop and squashed into a single
commit, on a branch named for the final approach.

Closes #15353

🤖 Generated with Claude Code

… with a 400

Sorting GET /admin/orders by a computed field — any total (subtotal,
tax_total, item_*, shipping_*, original_*, credit_line_*, ...),
payment_status, or fulfillment_status — threw a 500 because these are
not persisted columns on the order table (MikroORM: "Trying to order
by not existing property"). Fixes medusajs#15353.

- Add optional forbiddenOrderBy to QueryConfig (@medusajs/types): a
  sort-only deny list that does not affect field selection.
- Enforce it in prepareListQuery (@medusajs/framework) alongside the
  untouched allowed check, so no other route changes behavior.
- Define forbiddenAdminOrderSortFields on the orders list config:
  computed totals (mirroring shouldIncludeTotals in the order module
  service), payment_status, fulfillment_status, and summary.

A deny list rather than an allow list keeps this non-breaking: every
forbidden field previously 500'd, while real columns (email, status,
currency_code, ...) keep sorting as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hhuang2088
hhuang2088 requested a review from a team as a code owner July 15, 2026 23:32
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4b386ac

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 79 packages
Name Type
@medusajs/framework Patch
@medusajs/medusa Patch
@medusajs/types Patch
@medusajs/test-utils Patch
@medusajs/analytics Patch
@medusajs/api-key Patch
@medusajs/auth Patch
@medusajs/cache-inmemory Patch
@medusajs/cache-redis Patch
@medusajs/caching Patch
@medusajs/cart Patch
@medusajs/currency Patch
@medusajs/customer Patch
@medusajs/event-bus-local Patch
@medusajs/event-bus-redis Patch
@medusajs/file Patch
@medusajs/fulfillment Patch
@medusajs/index Patch
@medusajs/inventory Patch
@medusajs/link-modules Patch
@medusajs/locking Patch
@medusajs/notification Patch
@medusajs/order Patch
@medusajs/payment Patch
@medusajs/pricing Patch
@medusajs/product Patch
@medusajs/promotion Patch
@medusajs/rbac Patch
@medusajs/region Patch
@medusajs/sales-channel Patch
@medusajs/settings Patch
@medusajs/stock-location Patch
@medusajs/store Patch
@medusajs/tax Patch
@medusajs/translation Patch
@medusajs/user Patch
@medusajs/workflow-engine-inmemory Patch
@medusajs/workflow-engine-redis Patch
@medusajs/analytics-local Patch
@medusajs/analytics-posthog Patch
@medusajs/auth-emailpass Patch
@medusajs/auth-github Patch
@medusajs/auth-google Patch
@medusajs/caching-redis Patch
@medusajs/file-local Patch
@medusajs/file-s3 Patch
@medusajs/fulfillment-manual Patch
@medusajs/locking-postgres Patch
@medusajs/locking-redis Patch
@medusajs/notification-local Patch
@medusajs/notification-sendgrid Patch
@medusajs/payment-stripe Patch
@medusajs/draft-order Patch
@medusajs/loyalty-plugin Patch
@medusajs/core-flows Patch
integration-tests-http Patch
@medusajs/medusa-oas-cli Patch
@medusajs/js-sdk Patch
@medusajs/modules-sdk Patch
@medusajs/orchestration Patch
@medusajs/query Patch
@medusajs/utils Patch
@medusajs/workflows-sdk Patch
@medusajs/admin-bundler Patch
@medusajs/dashboard Patch
@medusajs/oas-github-ci Patch
@medusajs/http-types-generator Patch
@medusajs/cli Patch
@medusajs/deps Patch
@medusajs/eslint-plugin Patch
@medusajs/telemetry Patch
@medusajs/admin-sdk Patch
@medusajs/admin-shared Patch
@medusajs/admin-vite-plugin Patch
@medusajs/icons Patch
@medusajs/toolbox Patch
@medusajs/ui-preset Patch
create-medusa-app Patch
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@hhuang2088

Copy link
Copy Markdown
Author

@NicolasGorga following up on your review of #15809, which this PR supersedes: reworked as you suggested. QueryConfig now exposes forbiddenOrderBy instead of an allow list, enforced in prepareListQuery alongside the untouched allowed check — so sorting by any real column (email, status, currency_code, …) keeps working exactly as before.

One extension beyond the three fields you mentioned: an automated review pointed out that the other computed totals (subtotal, tax_total, item_*, shipping_*, original_*, credit_line_*, …) hit the same bug class, so the deny list covers the full computed set (mirroring shouldIncludeTotals in the order module service) plus payment_status, fulfillment_status, and summary. All of these previously threw 500s, so forbidding them changes no working request — the non-breaking property you flagged is preserved. Integration tests assert 400 for seven computed fields and 200 for real columns to lock that in.

@medusa-os-bot

medusa-os-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

Thanks for the contribution! Initial automated review looks good.

Cleanly guards the admin orders list against sorting by computed fields (totals, payment_status, fulfillment_status, summary) that previously threw a 500 at the DB layer, returning a 400 instead. Adds an optional forbiddenOrderBy to QueryConfig enforced in prepareListQuery alongside the untouched allowed check. Deny-list approach is non-breaking: every listed field already errored, so no working sort changes. Order-field parsing correctly strips leading '-'; nested sort keys won't false-match. PR template is complete, changeset included, and both unit and integration tests cover forbidden and

Triggered by: new PR opened

@shahednasser shahednasser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution @hhuang2088 ! Works well but just one comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you apply this to the store orders API route as well?

hhuang2088 and others added 2 commits July 22, 2026 16:26
Apply the admin orders sort deny list to GET /store/orders as well. The
store list route runs through the same getOrdersListWorkflow and
prepareListQuery path, so sorting by a computed field (any total,
payment_status, fulfillment_status, summary) 500'd the same way — these
are not persisted columns on the order table.

- Define forbiddenStoreOrderSortFields on the store orders list config,
  mirroring forbiddenAdminOrderSortFields (kept in sync per the file's
  existing "copied over from admin" convention). Includes
  discount_subtotal, a computed field the store surface exposes.
- Add integration tests asserting a 400 for computed sort fields and a
  200 for real columns (created_at, display_id, status).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hhuang2088
hhuang2088 requested a review from shahednasser July 22, 2026 23:39
Comment on lines 462 to 471
/**
* Fields rejected in the `order` query param (sorting). Unlike `allowed`,
* this does NOT affect field selection — a field can stay selectable while
* being forbidden as a sort key. Sorting by a field in this list throws
* INVALID_DATA (400).
*/
forbiddenOrderBy?: string[]
* Fields and relations that must never be resolved, regardless of what the
* caller requests. Any requested field whose path contains one of these
* segments (e.g. `orders` matches `orders.customer.email`) is stripped before

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's incorrect TSDoc block opening here which seems like it's due to merging develop and resolving conflicts incorrectly. Can you resolve it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Error trying to sort orders by: Total / Fulfillment status / Payment status

2 participants