Skip to main content

Working with OData

ยท 4 min read
zlike

A typical usage of overreact within our team is to deal with OData from various service endpoints. Usually these endpoints already have pre-built schema packages available (such as @bingads-webui/mca-odata-schemas, and @bingads-webui/campaign-odata-schemas), from which we can extract useful information and generate overreact specs without having our developers write from scratch. Given each entity (e.g., Activity, Ad) have CRUD operations, as well as OData actions/functions attached, this could save a tremendous amount of manual effort.

The Idea#

To generate a spec for each OData model, we'll need to solve these problems:

  1. How to create dataPath from OData model hierarchy.
  2. How to assign proper "Key" values to each level on hierarchy.
  3. How to identify from response which property is the "Key".

In overreact, the internal data structure ("store") is constructed from a schema tree, where each node has an associated dataPath to describe its location from root. Similarly, OData also organizes data using a tree-like structure, and provides navigation properties to locate specific data in the tree.

Consider an OData GET request to fetch an Activity. The URL would look like this:

GET https://contoso.com/Customers(123)/Accounts(456)/Activities('789')

We can directly map dataPath from EDM hierarchy to customer:account:activity.

For OData actions/functions, a call to https://contoso.com/Customers(123)/Accounts(456)/Default.FooBar() will map to customer:account:foo_bar. Note that we converted the Pascal naming to Snake convention, and discarded the namespace "Default" in this case.

The "Key" values are used to identify which entity to use on each level in the hierarchy. Currently in overreact we have 2 options to select keys:

  1. Using locator.order in variables. For example:
const variables = {    locator: {        order: ['cid', 'aid', 'activityId'],        descriptor: { cid: 123, aid: 456, activityId: '789' },    },};
  1. Using parentKeySelector in request contract.

Unfortunately parentKeySelector only provides "Key" info for current value, as well as its "parent" key values. We'll loose info for levels that are higher than 2, so in our case we'll resort to using order.

Finally, we need to identify the "Key" property value from OData responses, as they are used to look up cached items in overreact store. Luckily it is usually specified in $$ODataExtension.Key from the OData schemas. It is an array value but for now we'll only leverage the first one:

const { $$ODataExtension } = entitySchema;
createResponseContract({    // ...    keySelector: r => r[$$ODataExtension.Key[0]],})

Usage#

First, we'll need to define a "name mapper", to map OData schema name (Model/McaCustomer, Edm/String, etc.) to snake-case names ("customer", "string", "load_thread", etc.)

const pascalToSnakeCase = str => str.split(/(?=[A-Z])/).join('_').toLowerCase();
function schemaNameMapper(name) {  const MODEL_PREFIX = 'Model/';  const PRIMITIVE_PREFIX = 'Edm/';  const MCA_PREFIX = 'Mca';
  let modelName = name;  if (name.startsWith(MODEL_PREFIX)) {    modelName = name.substring(MODEL_PREFIX.length);
    if (modelName.startsWith(MCA_PREFIX)) {      modelName = modelName.substring(MCA_PREFIX.length);    }  }  if (name.startsWith(PRIMITIVE_PREFIX)) {    modelName = name.substring(PRIMITIVE_PREFIX.length);  }
  return pascalToSnakeCase(modelName);}

Given you've already set up the EDM model, simply create overreact schema and specs by doing this:

import {  createSpec,  createOverreactSchema,} from '@microsoft/overreact-odata';
const overreactSchema = createOverreactSchema(edm, schemaNameMapper, {  disapproved_campaign: 'Model/McaCampaign',});
const specs = createSpec(  // EDM model from @bingads-webui/edm-core  edm,      overreactSchema,
  // "schemas" is exported from OData schema packages, such as  //   import { schemas } from '@bingads-webui/mca-odata-schemas';  schemas['Model/McaCustomer'],
  schemaNameMapper,
  // Root property  [{ name: 'Customers', schema: schemas['Model/McaCustomer'] }],  {}    // reserved for future use);
// Debug only: dump the specs object to see what's been generated:console.log(specs);

Now, in your container component, simply do this:

import {    useODataAction,    useODataEntity,    useODataCollection,} from '@microsoft/overreact-odata';
const useReplyAsComment = variables => useODataAction(specs, 'customer:account:activity:reply_as_comment', null, variables);
const useActivity = (variables, config) => useODataEntity(specs, 'customer:account:activity', variables, config);
const useActivities = (variables, config) => useODataCollection(specs, 'customer:account:activity', variables, config);
const testVariableEntity = {    locator: {        descriptor: { cid: 123, aid: 456, activityId: '789' },        order: ['cid', 'aid', 'activityId'],    },};
const testVariableCollection = {    locator: {        descriptor: { cid: 123, aid: 456 },        order: ['cid', 'aid'],    },    pageSize: 20,    cursorIndex: 0,};
export function ActivityItem() {  const [{ data }, { update }] = useActivity(testVariableEntity);
  const updateData = useCallback(() => {    update({      Id: '123',      Title: 'Not fake anymore',      Author: 'Me',    });  }, [update]);
  return (    <div>      <pre>{data && JSON.stringify(data, null, 2)}</pre>      <button onClick={updateData}>Update</button>    </div>  );}
export function ActivityList() {  const [{ data }, { loadMore }] =     useActivities(testVariableCollection);
  useEffect(() => {    loadMore();  }, [loadMore]);
  return (    <div>      <pre>{data && JSON.stringify(data, null, 2)}</pre>    </div>  );}
export function ActivityReplyAsComment() {  const [data] = useReplyAsComment(testVariableEntity);
  return (    <div>      <pre>{data && JSON.stringify(data, null, 2)}</pre>    </div>  );}