IProductOrderGET Apex Interface

The IProductOrderGET Apex interface provides extensibility for TMF622 GET operations through Apex pre- and post-hooks and GraphQL query customization. Implementations can validate incoming requests, modify the primary read query, and refine or overwrite the response to support business-specific retrieval rules while maintaining TMF-compliant behavior.

This interface supports the following hooks.

  • transformRequest
  • applyCustomValidations
  • customiseGraphQLQuery
  • handlePostOperation

Note

The configureDefaultValidations hook is not used for Product Order GET. getDefaultValidationSpec() returns {}. If implemented, return the map unchanged.

Retrieval Lifecycle Use Cases and Hook Mapping 

This table lists common use cases and the hooks required for the GET operation.

Use CaseHook(s)DescriptionExample ScenariosBenefit
Resolve a legacy or alias order idtransformRequestReplace context["id"] with the canonical Order identifier before GraphQL construction.• Map LEGACY-* / PoNumber to Order.Id
• Accept a partner order id and resolve OrderNumber
Fetch the canonical Order without changing the client URL contract
Stash an entitlement flagtransformRequestPut extra keys on the shared context map for later hooks.isPremiumTenant for validation and post-processingCross-hook coordination without extra round-trips
Register a custom sub-resourcetransformRequest, customiseGraphQLQueryPut customSubResourceMappings on context and inject the matching GraphQL node. Both steps are required.• Serve GET /productOrder/{id}/orderAdjustmentGroupExtend GET with org-specific child resources
Reject the request with a business ruleapplyCustomValidationsOnly GET hook that surfaces an HTTP error. Return top-level validationStatus = "fail".• Cap filter criteria
• Forbid jeopardy-alert fields for non-premium tenants
Stops invalid retrievals before GraphQL
Add a custom field to the Order querycustomiseGraphQLQueryReturn nodes targeting handler aliases (productOrder, productOrder.productOrderItem, …).• Add NegotiatedPrice__c { value } at the root OrderShape the primary read without forking the handler
Filter or sort nested order itemscustomiseGraphQLQueryaddFilters / orderBy on productOrder.productOrderItem. URL sort still wins over Apex orderBy.• Quantity greater than 1
• EffectiveDate DESC
One API supports channel-specific query shape
Overwrite the GET bodyhandlePostOperationGET applies the return when payloadOverwritten is true. Single GET: Map. List GET: recordList or a raw List of maps.• Add customStatusLabel
• Drop Cancelled rows from list GET
Refine or filter the client-visible TMF payload

transformRequest 

This hook transforms the request context before processing any operation. It is invoked early in the request lifecycle, allowing implementers to adjust the context for subsequent extensibility hooks.

Mutating context["id"] is written back via updateIdFromContext. If the Apex result throws, the exception is caught and logged as WARNING — the original id is kept and GET continues.

Hook Method 

Map<String, Object> transformRequest(Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> transformRequest(Map<String, Object> context) {
2    if (context == null) {
3        return context;
4    }
5
6    context.put('isPremiumTenant', checkPremiumEntitlement());
7
8    String requestedId = (String) context.get('id');
9    if (requestedId != null && requestedId.startsWith('LEGACY-')) {
10        Order o = [SELECT Id FROM Order WHERE PoNumber = :requestedId LIMIT 1];
11        context.put('id', o.Id);
12    }
13    return context;
14}

Caveat. Appending to context["fields"] does not change the primary GraphQL query. This handler hardcodes the query in buildPrimaryGraphQLQuery. Use customiseGraphQLQuery.

applyCustomValidations 

This hook validates custom business logic before retrieving a Product Order. If validation fails, it rejects the request and returns an error response.

The handler processes return values as follows.

Success

  • Return a map with validationStatus set to any value other than "fail" (case-insensitive), or an empty map.
  • The API request continues normally.
  • Other fields in the map are logged but not used.

Failure

  • Return a map with validationStatus set to "fail" (case-insensitive).
  • The API request is terminated immediately.
  • Raise a ValidationException containing:
    • message: value from validationMessage (or “Validation failed” by default)
    • details: value from validationDetails (optional statusCode / errorCode)

Hook Method 

Map<String, Object> applyCustomValidations(Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
2    Boolean isPremium = (Boolean) context.get('isPremiumTenant');
3    List<Object> fields = (List<Object>) context.get('fields');
4
5    if (isPremium != Boolean.TRUE && fields != null
6            && fields.contains('productorderjeopardyalert')) {
7        return new Map<String, Object>{
8            'validationStatus'  => 'fail',
9            'validationMessage' => 'Jeopardy alerts require a premium entitlement.',
10            'validationDetails' => new Map<String, Object>{ 'statusCode' => 'FORBIDDEN' }
11        };
12    }
13    return new Map<String, Object>();
14}

customiseGraphQLQuery 

This hook modifies the primary GraphQL read query before execution. GET is the only Product Order verb that builds an initial read query.

Return {"nodes": [ QueryTransformationNode, ... ]}. The graphQLAsMap argument is {"query": "<base query>"} — informational only. There is no supported way to hand back a rewritten query string.

Node shape

1{
2  "path": "productOrder.productOrderItem",
3  "addFields": ["CustomField__c { value }"],
4  "addFilters": { "clearExistingFilters": false, "filterGraphQlSnippet": "Status: {eq: \"Active\"}" },
5  "orderBy": { "clearExistingOrderBy": false, "orderGraphQlSnippet": "CreatedDate: {order: DESC}" },
6  "insertGraphQlSnippet": "customField: CustomObject__r { Id Name { value } }"
7}

path addresses aliases, not Salesforce relationship names. Addressable paths:

Alias pathWhat it addresses
productOrderRoot Order alias in buildPrimaryGraphQLQuery
productOrder.relatedPartyAccountAccount
productOrder.orderDeliveryGroupsOrderDeliveryGroups
productOrder.productOrderItemOrderItems
productOrder.productOrderItem.itemOrderActionOrderAction
productOrder.productOrderItem.itemProductProduct2
productOrder.productOrderItem.itemPricebookEntryPricebookEntry
productOrder.productOrderItem.itemPricebookEntry.itemProductSellingModelProductSellingModel

An unmatched path silently produces no transformation for that node (WARNING log). Auxiliary queries (jeopardy alerts, order-item attributes, relationships, tax lines) run inside executeGraphQL after this hook and cannot be transformed here.

Hook Method 

Map<String, Object> customiseGraphQLQuery(Map<String, Object> graphQLAsMap, Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> customiseGraphQLQuery(
2        Map<String, Object> graphQLAsMap, Map<String, Object> context) {
3    return new Map<String, Object>{
4        'nodes' => new List<Object>{
5            new Map<String, Object>{
6                'path'      => 'productOrder',
7                'addFields' => new List<Object>{ 'NegotiatedPrice__c { value }' }
8            },
9            new Map<String, Object>{
10                'path'       => 'productOrder.productOrderItem',
11                'addFilters' => new Map<String, Object>{
12                    'clearExistingFilters' => false,
13                    'filterGraphQlSnippet' => 'Quantity: {gt: 1}'
14                }
15            }
16        }
17    };
18}

handlePostOperation 

This hook post-processes the API response after GraphQL execution and TMF conversion. Unlike POST, PATCH, and DELETE, GET applies this return value when payloadOverwritten is true.

Parameters

  • graphQLQueryResultAsMap — raw GraphQL {data, errors, extensions}
  • constructedTMFResponse — already-TMF622-shaped output. Single GET: flat map. List GET: {"recordList": [...]}.
  • context — the same map from earlier hooks

Return value processing

  • { "payloadOverwritten": false } (or omit the key) — observe only; output unchanged.
  • { "payloadOverwritten": true, "updatedPayload": ... } — replace the response.
    • Single-entity: updatedPayload must be a Map.
    • List: updatedPayload must contain recordList as a List, or be a raw List of maps.
  • Wrong type logs WARNING and no-ops. The caller receives the original response.

Hook Method 

Map<String, Object> handlePostOperation(Map<String, Object> graphQLQueryResultAsMap, Map<String, Object> constructedTMFResponse, Map<String, Object> context)

Sample Apex Implementation 

1global static Map<String, Object> handlePostOperation(
2        Map<String, Object> graphQLQueryResultAsMap,
3        Map<String, Object> constructedTMFResponse,
4        Map<String, Object> context) {
5    if (constructedTMFResponse.containsKey('recordList')) {
6        List<Object> recordList = (List<Object>) constructedTMFResponse.get('recordList');
7        List<Object> filtered = new List<Object>();
8        for (Object rec : recordList) {
9            Map<String, Object> recMap = (Map<String, Object>) rec;
10            if (recMap.get('state') != 'Cancelled') {
11                filtered.add(recMap);
12            }
13        }
14        return new Map<String, Object>{
15            'payloadOverwritten' => true,
16            'updatedPayload'     => new Map<String, Object>{ 'recordList' => filtered }
17        };
18    }
19
20    Map<String, Object> updated = new Map<String, Object>(constructedTMFResponse);
21    updated.put('customStatusLabel', deriveFriendlyStatus((String) updated.get('state')));
22    return new Map<String, Object>{
23        'payloadOverwritten' => true,
24        'updatedPayload'     => updated
25    };
26}

Full Implementation Example 

1global class ProductOrderGETExtension implements comms_apex_ext.IProductOrderGET {
2
3    global static Map<String, Object> transformRequest(Map<String, Object> context) {
4        context.put('isPremiumTenant', checkPremiumEntitlement());
5        String requestedId = (String) context.get('id');
6        if (requestedId != null && requestedId.startsWith('LEGACY-')) {
7            Order o = [SELECT Id FROM Order WHERE PoNumber = :requestedId LIMIT 1];
8            context.put('id', o.Id);
9        }
10        return context;
11    }
12
13    global static Map<String, Boolean> configureDefaultValidations(
14            Map<String, Boolean> defaultValidationConfiguration,
15            Map<String, Object> context) {
16        return defaultValidationConfiguration;
17    }
18
19    global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
20        Boolean isPremium = (Boolean) context.get('isPremiumTenant');
21        List<Object> fields = (List<Object>) context.get('fields');
22        if (isPremium != Boolean.TRUE && fields != null
23                && fields.contains('productorderjeopardyalert')) {
24            return new Map<String, Object>{
25                'validationStatus'  => 'fail',
26                'validationMessage' => 'Jeopardy alerts require a premium entitlement.',
27                'validationDetails' => new Map<String, Object>{ 'statusCode' => 'FORBIDDEN' }
28            };
29        }
30        return new Map<String, Object>();
31    }
32
33    global static Map<String, Object> customiseGraphQLQuery(
34            Map<String, Object> graphQLAsMap, Map<String, Object> context) {
35        return new Map<String, Object>{
36            'nodes' => new List<Object>{
37                new Map<String, Object>{
38                    'path'      => 'productOrder',
39                    'addFields' => new List<Object>{ 'NegotiatedPrice__c { value }' }
40                }
41            }
42        };
43    }
44
45    global static Map<String, Object> handlePostOperation(
46            Map<String, Object> graphQLQueryResultAsMap,
47            Map<String, Object> constructedTMFResponse,
48            Map<String, Object> context) {
49        System.debug('ProductOrder GET by ' + UserInfo.getName());
50        return new Map<String, Object>{ 'payloadOverwritten' => false };
51    }
52}

GraphQL Query Transformations 

Query transformations enhance GraphQL queries by adding fields, applying filters, or modifying sort order, while preserving the original query definition.

Note

For Product Order GET, use handler aliases such as productOrder and productOrder.productOrderItem. Do not use Salesforce relationship names or uiapi.query.Order.

Query Enhancements: Add Fields 

Original Query (GraphQL): primary Order selection from buildPrimaryGraphQLQuery.

Transformation Instructions (JSON):

1{
2  "nodes": [{
3    "path": "productOrder",
4    "addFields": ["NegotiatedPrice__c { value }"]
5  }]
6}

The field is appended as a sibling of the scalar fields on the root Order alias.

Query Enhancements: Nested Filter and Root orderBy 

Transformation Instructions (JSON):

1{
2  "nodes": [
3    {
4      "path": "productOrder",
5      "orderBy": {
6        "clearExistingOrderBy": true,
7        "orderGraphQlSnippet": "EffectiveDate: {order: DESC}"
8      }
9    },
10    {
11      "path": "productOrder.productOrderItem",
12      "addFilters": {
13        "clearExistingFilters": false,
14        "filterGraphQlSnippet": "Quantity: {gt: 1}"
15      }
16    }
17  ]
18}

URL-level sort takes precedence over Apex orderBy. A trailing Id ASC tiebreaker is always appended.

See Also