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 Case | Hook(s) | Description | Example Scenarios | Benefit |
|---|---|---|---|---|
| Resolve a legacy or alias order id | transformRequest |
Replace context["id"] with the canonical Order identifier before GraphQL construction. |
• Map LEGACY-* / PoNumber to Order.Id<br>• Accept a partner order id and resolve OrderNumber |
Fetch the canonical Order without changing the client URL contract |
| Stash an entitlement flag | transformRequest |
Put extra keys on the shared context map for later hooks. |
• isPremiumTenant for validation and post-processing |
Cross-hook coordination without extra round-trips |
| Register a custom sub-resource | transformRequest, customiseGraphQLQuery |
Put customSubResourceMappings on context and inject the matching GraphQL node. Both steps are required. |
• Serve GET /productOrder/{id}/orderAdjustmentGroup |
Extend GET with org-specific child resources |
| Reject the request with a business rule | applyCustomValidations |
Only GET hook that surfaces an HTTP error. Return top-level validationStatus = "fail". |
• Cap filter criteria<br>• Forbid jeopardy-alert fields for non-premium tenants | Stops invalid retrievals before GraphQL |
| Add a custom field to the Order query | customiseGraphQLQuery |
Return nodes targeting handler aliases (productOrder, productOrder.productOrderItem, …). |
• Add NegotiatedPrice__c { value } at the root Order |
Shape the primary read without forking the handler |
| Filter or sort nested order items | customiseGraphQLQuery |
addFilters / orderBy on productOrder.productOrderItem. URL sort still wins over Apex orderBy. |
• Quantity greater than 1<br>• EffectiveDate DESC | One API supports channel-specific query shape |
| Overwrite the GET body | handlePostOperation |
GET applies the return when payloadOverwritten is true. Single GET: Map. List GET: recordList or a raw List of maps. |
• Add customStatusLabel<br>• 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
global static Map<String, Object> transformRequest(Map<String, Object> context) {
if (context == null) {
return context;
}
context.put('isPremiumTenant', checkPremiumEntitlement());
String requestedId = (String) context.get('id');
if (requestedId != null && requestedId.startsWith('LEGACY-')) {
Order o = [SELECT Id FROM Order WHERE PoNumber = :requestedId LIMIT 1];
context.put('id', o.Id);
}
return context;
}
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
validationStatusset 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
validationStatusset to"fail"(case-insensitive). - The API request is terminated immediately.
- Raise a
ValidationExceptioncontaining:- message: value from
validationMessage(or “Validation failed” by default) - details: value from
validationDetails(optionalstatusCode/errorCode)
- message: value from
Hook Method
Map<String, Object> applyCustomValidations(Map<String, Object> context)
Sample Apex Implementation
global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
Boolean isPremium = (Boolean) context.get('isPremiumTenant');
List<Object> fields = (List<Object>) context.get('fields');
if (isPremium != Boolean.TRUE && fields != null
&& fields.contains('productorderjeopardyalert')) {
return new Map<String, Object>{
'validationStatus' => 'fail',
'validationMessage' => 'Jeopardy alerts require a premium entitlement.',
'validationDetails' => new Map<String, Object>{ 'statusCode' => 'FORBIDDEN' }
};
}
return new Map<String, Object>();
}
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
{
"path": "productOrder.productOrderItem",
"addFields": ["CustomField__c { value }"],
"addFilters": { "clearExistingFilters": false, "filterGraphQlSnippet": "Status: {eq: \"Active\"}" },
"orderBy": { "clearExistingOrderBy": false, "orderGraphQlSnippet": "CreatedDate: {order: DESC}" },
"insertGraphQlSnippet": "customField: CustomObject__r { Id Name { value } }"
}
path addresses aliases, not Salesforce relationship names. Addressable paths:
| Alias path | What it addresses |
|---|---|
| productOrder | Root Order alias in buildPrimaryGraphQLQuery |
| productOrder.relatedPartyAccount | Account |
| productOrder.orderDeliveryGroups | OrderDeliveryGroups |
| productOrder.productOrderItem | OrderItems |
| productOrder.productOrderItem.itemOrderAction | OrderAction |
| productOrder.productOrderItem.itemProduct | Product2 |
| productOrder.productOrderItem.itemPricebookEntry | PricebookEntry |
| productOrder.productOrderItem.itemPricebookEntry.itemProductSellingModel | ProductSellingModel |
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
global static Map<String, Object> customiseGraphQLQuery(
Map<String, Object> graphQLAsMap, Map<String, Object> context) {
return new Map<String, Object>{
'nodes' => new List<Object>{
new Map<String, Object>{
'path' => 'productOrder',
'addFields' => new List<Object>{ 'NegotiatedPrice__c { value }' }
},
new Map<String, Object>{
'path' => 'productOrder.productOrderItem',
'addFilters' => new Map<String, Object>{
'clearExistingFilters' => false,
'filterGraphQlSnippet' => 'Quantity: {gt: 1}'
}
}
}
};
}
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:
updatedPayloadmust be a Map. - List:
updatedPayloadmust containrecordListas a List, or be a raw List of maps.
- Single-entity:
- 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
global static Map<String, Object> handlePostOperation(
Map<String, Object> graphQLQueryResultAsMap,
Map<String, Object> constructedTMFResponse,
Map<String, Object> context) {
if (constructedTMFResponse.containsKey('recordList')) {
List<Object> recordList = (List<Object>) constructedTMFResponse.get('recordList');
List<Object> filtered = new List<Object>();
for (Object rec : recordList) {
Map<String, Object> recMap = (Map<String, Object>) rec;
if (recMap.get('state') != 'Cancelled') {
filtered.add(recMap);
}
}
return new Map<String, Object>{
'payloadOverwritten' => true,
'updatedPayload' => new Map<String, Object>{ 'recordList' => filtered }
};
}
Map<String, Object> updated = new Map<String, Object>(constructedTMFResponse);
updated.put('customStatusLabel', deriveFriendlyStatus((String) updated.get('state')));
return new Map<String, Object>{
'payloadOverwritten' => true,
'updatedPayload' => updated
};
}
Full Implementation Example
global class ProductOrderGETExtension implements comms_apex_ext.IProductOrderGET {
global static Map<String, Object> transformRequest(Map<String, Object> context) {
context.put('isPremiumTenant', checkPremiumEntitlement());
String requestedId = (String) context.get('id');
if (requestedId != null && requestedId.startsWith('LEGACY-')) {
Order o = [SELECT Id FROM Order WHERE PoNumber = :requestedId LIMIT 1];
context.put('id', o.Id);
}
return context;
}
global static Map<String, Boolean> configureDefaultValidations(
Map<String, Boolean> defaultValidationConfiguration,
Map<String, Object> context) {
return defaultValidationConfiguration;
}
global static Map<String, Object> applyCustomValidations(Map<String, Object> context) {
Boolean isPremium = (Boolean) context.get('isPremiumTenant');
List<Object> fields = (List<Object>) context.get('fields');
if (isPremium != Boolean.TRUE && fields != null
&& fields.contains('productorderjeopardyalert')) {
return new Map<String, Object>{
'validationStatus' => 'fail',
'validationMessage' => 'Jeopardy alerts require a premium entitlement.',
'validationDetails' => new Map<String, Object>{ 'statusCode' => 'FORBIDDEN' }
};
}
return new Map<String, Object>();
}
global static Map<String, Object> customiseGraphQLQuery(
Map<String, Object> graphQLAsMap, Map<String, Object> context) {
return new Map<String, Object>{
'nodes' => new List<Object>{
new Map<String, Object>{
'path' => 'productOrder',
'addFields' => new List<Object>{ 'NegotiatedPrice__c { value }' }
}
}
};
}
global static Map<String, Object> handlePostOperation(
Map<String, Object> graphQLQueryResultAsMap,
Map<String, Object> constructedTMFResponse,
Map<String, Object> context) {
System.debug('ProductOrder GET by ' + UserInfo.getName());
return new Map<String, Object>{ 'payloadOverwritten' => false };
}
}
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):
{
"nodes": [{
"path": "productOrder",
"addFields": ["NegotiatedPrice__c { value }"]
}]
}
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):
{
"nodes": [
{
"path": "productOrder",
"orderBy": {
"clearExistingOrderBy": true,
"orderGraphQlSnippet": "EffectiveDate: {order: DESC}"
}
},
{
"path": "productOrder.productOrderItem",
"addFilters": {
"clearExistingFilters": false,
"filterGraphQlSnippet": "Quantity: {gt: 1}"
}
}
]
}
URL-level sort takes precedence over Apex orderBy. A trailing Id ASC tiebreaker is always appended.