Skip to content

[5.8] GraphQL: settings queries, Commerce element queries/mutations, product drafts, and authenticated requests - #4397

Draft
brandonkelly wants to merge 15 commits into
5.8from
feature/graphql-improvements
Draft

brandonkelly wants to merge 15 commits into
5.8from
feature/graphql-improvements

Conversation

@brandonkelly

@brandonkelly brandonkelly commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Summary

Authorization: Bearer <JWT> ──▶ Gql::getSchemaForUser($user)   (craftcms/cms#20147)
                                   │  Commerce components with `authenticated`
                                   ▼
  queries                                  mutations
  productTypes.<uid>:read  viewProductType  productTypes.<uid>:create|save|delete  create/save/deleteProductType
  orders.<store>:read      everyone*        orders.<store>:create|save|delete      editOrders / deleteOrders
  donations.all:read       everyone         donations.all:save                     manageDonationSettings
  transfers.all:read       manageInventoryTransfers  transfers.all:create|save|delete  manageInventoryTransfers
  settings.producttypes    accessCp
  settings.stores, settings.orderstatuses   everyone
  elements.drafts / revisions (from cms)    accessCp

  * limited to the user's own orders unless they have “Manage orders”
  Mutations also run Elements::canSave()/canDelete()/canCreateDrafts() for the authenticated user.

Requires craftcms/cms dev-feature/authenticated-graphql (aliased as 5.12.0 so ckeditor/redactor resolve). commerceguys/addressing stays at 2.2.5.

Non-element queries (following craftcms/cms#20055):

Component Queries Element field
settings.producttypes:read productTypes, productType productType on products
settings.stores:read stores, store store on orders and order statuses
settings.orderstatuses:read orderStatuses, orderStatus orderStatus on orders

Element queries and mutations:

Element Queries Mutations
Products, variants (existing), plus draft/revision fields save_<type>_Product (with sortOrder + variants), save_<type>_ProductDraft, createProductDraft, publishProductDraft, save_<type>_Variant, deleteProduct, deleteVariant
Orders orders, order, orderCount save_Order (customer, status, coupon, shipping method, addresses, lineItems: [...], isCompleted), deleteOrder
Donation donation save_Donation
Inventory transfers transfers, transfer, transferCount save_Transfer (locations, details: [...], markAsPending), deleteTransfer

Subscriptions are intentionally left out, since they're more or less deprecated.

Notes for review:

  • Variants work like Matrix fields. sortOrder lists the variant IDs the product should have, in order, with new:1, new:2… for new ones. variants holds data for new and changed variants only. Variants missing from sortOrder are deleted; without sortOrder, the existing variants are kept and only updated.
  • Product drafts work like entry drafts. Same arguments (draftId, provisional, asUnpublishedDraft, draftName, draftNotes), and the same draft mutations under product-specific names. Variants changed through a draft are duplicated as drafts first, like Matrix's nested entries, so the live product is untouched until the draft is published. Authenticated requests always create drafts as the authenticated user and only see their own provisional drafts. Product::canCreateDrafts() returns true for everyone, so any user with the product type's save permission can edit another user's saved draft. The control panel behaves the same way.
  • lineItems and details still take a full list: items with an id are updated, items without one are created, and the rest are removed.
  • save_Order with isCompleted: true calls markAsComplete() after saving, like OrdersController::actionSave(). email creates a user via ensureUserByEmail(), so user authorization is checked before anything is changed. New orders get the remote origin.
  • Transfers: only drafts can change locations/items or be deleted, since markAsPending updates inventory in Transfer::afterSave(). Receiving stays in the control panel.
  • Bug fix: Transfer::canView/canSave/canDelete checked commerce-manageTransfers, but the registered permission (and TransfersController) is commerce-manageInventoryTransfers. This blocked authenticated transfer mutations, so they now check the registered permission.
  • Donation::canSave() is new (“Manage donation settings”), so element authorization works for authenticated requests.
  • Not included: custom (non-purchasable) line items, order notices, receiving transfers, variant drafts outside of product drafts.
  • productType (products) and lineItems, orderStatus, store (orders) are now reserved field handles.
  • The product/variant type generators now implement SingleGeneratorInterface (generateType()), so mutations can return a specific product type's type.
  • Also fixes the product/variant field layout fixtures, which used craft\elements\Product/Variant, so product GraphQL queries can run in tests.

Evidence

tests/unit/gql/AuthenticatedSchemaTest.php and tests/unit/gql/MutationsTest.php (fixtures, Gql::executeQuery):

  • Authenticated reads: a user with commerce-viewProductType on T-Shirts only gets T-shirt products/variants; orderCount is 3 for the owner, 0 for another customer, 3 with commerce-manageOrders.
  • save_tShirts_Product(sortOrder: ["new:2", "new:1"], variants: [...]) creates the variants in that order. variants without sortOrder updates one variant and ignores the unlisted new one. sortOrder: [$large] deletes the other variant.
  • Product drafts as an authenticated user with accessCp + save permission: createProductDraft ignores creatorId; save_tShirts_ProductDraft changes the title and a variant's price while the live product and variant stay unchanged; products(drafts: true) returns the draft; publishProductDraft applies both.
  • An authenticated user with saveProductType but not createProductType can update a product but gets Unable to perform the action. when creating one.
  • save_Order(email, lineItems) → cart; then lineItems: [{purchasableId: blue, qty: 3}], isCompleted: true → completed, dateOrdered set, only hct-blue remains. A customer without “Edit orders” gets no orders.*:save scope; with it, save_Order saves.
  • An authenticated user with “Manage inventory transfers” creates a draft transfer and marks it pending; changing its items or deleting it then fails.
  • Before/after: removing the own-orders filter (3 is identical to 0), the variant draft duplication (Failed asserting that 99.0 matches expected 19.99), the pending-transfer lock, or the transfer permission fix (save_Transfer → Unable to perform the action.) each fails its test. With them: all pass.

Scratch checks (not committed):

  • save_tShirts_ProductDraft(asUnpublishedDraft: true, …) with a new variant, then publishProductDraft, gives a live product with that variant.
  • With authenticated schemas, stores and order statuses are available to everyone, product types need accessCp, and save_Donation needs “Manage donation settings”.
  • Address validation errors come back as GraphQL errors, and orders can't be created in stores outside the schema.

brandonkelly and others added 8 commits October 29, 2026 00:26
Follows craftcms/cms#20055: each gets a settings.* schema component that adds
plural/singular queries. Product types also add a productType field to products.

Also fixes the product/variant field layout fixtures, which had the wrong
element types, so product GraphQL queries can run in tests.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
Adds orders/order/orderCount queries, enabled per store via orders.<storeUid>:read
schema components. Orders expose their line items, addresses, and (when the
schema allows) their store and order status.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
Follows craftcms/cms#20147: Commerce's schema components now declare an
`authenticated` key, so OAuth-authenticated requests get access based on the
user's permissions.

- Product types map to the “View products” permission for each product type
- “Query for product types” requires control panel access, like CMS sections
- Stores and order statuses are available to everyone
- Orders are available to everyone, but limited to the user's own orders unless
  they have the “Manage orders” permission

Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
The craftcms/cms update pulled in addressing 2.3.2, whose updated GB
subdivisions break StoreTest. Nothing here needs the newer version.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
- save_<productType>_Product creates/updates products. Its `variants` argument
  sets the full list of variants, like saving the product in the control panel,
  which is needed to create a product (live products require a variant).
- save_<productType>_Variant saves an individual variant, and deleteProduct and
  deleteVariant delete them.
- New per-product-type create/save/delete schema components, mapped to the
  matching product type permissions for authenticated requests.

The product and variant type generators now implement SingleGeneratorInterface
so mutations can return a specific product type's GraphQL type.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
save_Order follows the control panel's order save: it sets the customer (by ID,
or by email, creating a user if needed), status, coupon, shipping method,
addresses, custom fields, and the full list of line items, and `isCompleted:
true` marks the order as complete after saving. New orders get the `remote`
origin. deleteOrder deletes an order.

Per-store create/save/delete schema components map to the “Edit orders” and
“Delete orders” permissions for authenticated requests.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
…fers

These element types had no GraphQL support, so each gets a type, queries, and
mutations:

- Subscriptions: query, save custom fields, and delete (without canceling on
  the gateway, like the control panel's default). Authenticated users only see
  their own subscriptions unless they can manage subscriptions.
- Donation: query, and save its SKU, availability, and status. Donation now
  implements canSave() based on “Manage donation settings”.
- Inventory transfers: query, create/save drafts with their items, mark them
  as pending, and delete drafts. Pending transfers' locations and items are
  locked, since marking them pending updates inventory.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
@brandonkelly brandonkelly changed the title GraphQL: settings queries, orders, and authenticated requests GraphQL: settings queries, Commerce element queries/mutations, and authenticated requests Oct 29, 2026
brandonkelly and others added 5 commits October 29, 2026 06:07
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Co-authored-by: Amp <amp@ampcode.com>
Subscriptions are more or less deprecated, so they don’t get GraphQL queries
or mutations after all.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Product save mutations now take a `sortOrder` argument, which determines the
product's variants and their order, and a `variants` argument with data for
new and updated variants, keyed by ID or `new:N`, like Matrix fields'
`sortOrder` and `entries`.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Products get the same draft support as entries:

- `save_<productType>_ProductDraft` saves existing drafts (by `draftId`) or
  creates unpublished drafts (`asUnpublishedDraft`).
- `createProductDraft` and `publishProductDraft` create and apply drafts.
- Products' GraphQL data includes the draft and revision fields.

Variants changed via a draft are duplicated as drafts first, like Matrix
entries, so the canonical product's variants stay untouched until the draft is
published. Authenticated requests always create drafts for the authenticated
user, and only see their own provisional drafts.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Transfer::canView(), canSave(), and canDelete() checked
`commerce-manageTransfers`, but the registered permission (also checked by
TransfersController) is `commerce-manageInventoryTransfers`. Only users who
had the permission before the m240815_035618_fix_transfer_permission migration
had both, so authenticated GraphQL requests couldn't manage transfers.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
@brandonkelly brandonkelly changed the title GraphQL: settings queries, Commerce element queries/mutations, and authenticated requests GraphQL: settings queries, Commerce element queries/mutations, product drafts, and authenticated requests Oct 29, 2026
@lukeholder lukeholder changed the title GraphQL: settings queries, Commerce element queries/mutations, product drafts, and authenticated requests [5.8] GraphQL: settings queries, Commerce element queries/mutations, product drafts, and authenticated requests Oct 30, 2026
It shipped via 5.x (#4399), so it's already in CHANGELOG.md.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a1850a-a3a4-7642-a937-0ac503e1d04d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant