# OData

**index**

* [TypeScript signature](#typescript-signature)
* [Lifecycle Phases](#lifecycle-phases)
* [Usage](#grid-definition--call-of-backendserviceapi)
* [Passing Extra Arguments](#passing-extra-arguments-to-the-query)
* [OData options](#odata-options)
* [Override the filter query](#override-the-filter-query)
* [Infinite Scroll](/slickgrid-universal/grid-functionalities/infinite-scroll.md#infinite-scroll-with-backend-services)

#### Description

OData Backend Service (for Pagination purposes) to get data from a backend server with the help of OData.

#### Note

Use it when you need to support **Pagination** (that is when your dataset is rather large, more than 5k rows) with a OData endpoint. If your dataset is small (less than 5k rows), then go with a [regular grid](https://ghiscoding.github.io/slickgrid-universal/#/example01) with the "dataset.bind" property. SlickGrid can easily handle million of rows using a DataView object, but personally when the dataset is known to be large, I usually use a backend service (OData or GraphQL) and when it's small I go with a [regular grid](https://ghiscoding.github.io/slickgrid-universal/#/example01).

### Implementation

To connect a backend service into `Slickgrid-Universal`, you simply need to modify your `gridOptions` and add a declaration of `backendServiceApi` and pass it the `service`. See below for the signature and an example further down below.

#### Demo

[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example09) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/tree/master/demos/vanilla/src/examples/example09.ts)

#### IMPORTANT NOTE

All the code below assumes that your Backend Server (probably in C#) will return the data into an `items` property. You could return the array directly **but it is strongly discouraged to do that** because that will conflict with the `metrics` that you will see in the code below. The best approach is to return your data into a property, like `items` or any property name you wish to use, on your backend server side. Your result should have this kind of structure

```ts
{
  items: [ /* your data */ ]
}
```

#### Lifecycle Phases

All backend services follow these three lifecycle phases:

| Phase         | Description                                                                              |
| ------------- | ---------------------------------------------------------------------------------------- |
| `preProcess`  | Invoked before the query is processed (e.g., show a loading spinner).                    |
| `process`     | Generates the OData query string, which is then sent to your backend server.             |
| `postProcess` | Handles the response, updates the grid dataset, and typically stops the loading spinner. |

This structure ensures a consistent flow for data operations: start loading, process the query, and update the grid when data is received.

#### TypeScript Signature

```typescript
backendServiceApi: {
  // Backend Service instance (could be OData or GraphQL Service)
  service: BackendService;

  // add any options you might want to provide to the backend service
  options: OdataOption | GraphqlServiceOption;

  // On init (or on page load), what action to perform?
  onInit?: (query: string) => Promise<any>;

  // Before executing the query, what action to perform? For example, start a spinner
  preProcess?: () => void;

  // On Processing, we get the query back from the service, and we need to provide a Promise. For example: this.http.get(myGraphqlUrl)
  // Note: The optional second parameter includes an AbortSignal to cancel pending requests when a new filter/sort is triggered
  process: (query: string, options?: { signal?: AbortSignal }) => Promise<any>;

  // After executing the query, what action to perform? For example, stop the spinner
  postProcess: (response: any) => void;

  // Throttle the amount of requests sent to the backend. Default to 500ms
  filterTypingDebounce?: number;
}
```

As you can see, you mainly need to define which service to use (GridODataService or GraphQLService) and finally add the `process` and `postProcess` callback.

**Grid Definition & call of `backendServiceApi`**

**Notes**

* Pagination is optional and if not defined, it will use what is set in the [Slickgrid-Universal - Global Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/global-grid-options.ts)
* `onInit` is optional and is there to initialize (pre-populate) the grid with data on first page load (typically the same call as `process`)
  * you could load the grid yourself outside of the `gridOptions` which is why it's optional
* `filterTypingDebounce` is a timer (in milliseconds) that waits for user input pause before querying the backend server
  * this is meant to throttle the amount of requests sent to the backend (we don't really want to query every keystroke)
  * 700ms is the default when not provided

**Code**

```ts
import { GridOdataService, OdataServiceApi, OdataOption } from '@slickgrid-universal/odata';

export class Example {
  columns: Column[];
  gridOptions: GridOption;
  dataset = [];

  constructor(http) {
    this.http = http;

    // define the grid options & columns and then create the grid itself
    this.defineGrid();
  }

  defineGrid() {
    this.columns = [
      // your column definitions
    ];

    this.gridOptions = {
      enableFiltering: true,
      enablePagination: true,
      pagination: {
        pageSizes: [10, 15, 20, 25, 30, 40, 50, 75, 100],
        pageSize: defaultPageSize,
        totalItems: 0
      },
      backendServiceApi: {
        service: new GridOdataService(),
        // define all the on Event callbacks
        options: {
          caseType: CaseType.pascalCase,
          top: defaultPageSize
        } as OdataOption,
        preProcess: () => this.displaySpinner(true),
        process: (query, options) => this.getCustomerApiCall(query, options),
        postProcess: (response) => {
          this.displaySpinner(false);
          this.getCustomerCallback(response);
        }
      }
    };
  }

  // Web API call - note the second parameter contains the AbortSignal for request cancellation
  getCustomerApiCall(odataQuery: string, options?: { signal?: AbortSignal }) {
    // regular Http Client call
    return this.http.createRequest(`/api/customers?${odataQuery}`).then(response => response.json());

    // or with Fetch Client - supporting AbortSignal for request cancellation
    // return this.http.fetch(`/api/customers?${odataQuery}`, { signal: options?.signal }).then(response => response.json());
  }

  getCustomerCallback(response) {
    // totalItems property needs to be filled for pagination to work correctly
    // however we need to force the Framework to do a dirty check, doing a clone object will do just that
    let countPropName = 'totalRecordCount'; // you can use "totalRecordCount" or any name or "odata.count" when "enableCount" is set
    if (this.isCountEnabled) {
      countPropName = (this.odataVersion === 4) ? '@odata.count' : 'odata.count';
    }
    if (this.metrics) {
      this.metrics.totalItemCount = data[countPropName];
    }

    // once pagination totalItems is filled, we can update the dataset
    this.sgb.paginationOptions.totalItems = data[countPropName];
    this.sgb.dataset = data.items as Customer[];
  }
}
```

#### Request Cancellation with AbortSignal

When users trigger rapid filter or sort changes (e.g., typing quickly in a filter), SlickGrid will automatically cancel any pending HTTP requests using the `AbortSignal` API. This prevents stale results from overwriting newer data and improves user experience.

The `process` method receives an optional `options` parameter. If you want to support automatic request cancellation, pass the `signal` from this parameter to your fetch/HTTP call. When a new request is triggered, any previous `AbortSignal` will be aborted automatically.

**How It Works**

1. User types in a filter → sends "Jo" query (request #1)
2. User types "e" quickly before response arrives → "Joe" query (request #2)
3. SlickGrid automatically aborts request #1's signal
4. Request #2's response is processed, request #1's response is ignored

**Implementation Example with Fetch API**

```ts
getCustomerApiCall(odataQuery: string, options?: { signal?: AbortSignal }) {
  // Use the signal from options to enable automatic cancellation
  return fetch(`/api/customers?${odataQuery}`, {
    signal: options?.signal  // Pass the signal to abort the request
  }).then(response => response.json());
}
```

**Implementation with Axios**

```ts
getCustomerApiCall(odataQuery: string, options?: { signal?: AbortSignal }) {
  return axios.get(`/api/customers?${odataQuery}`, {
    signal: options?.signal  // Axios supports AbortSignal
  });
}
```

#### Passing Extra Arguments to the Query

You might need to pass extra arguments to your OData query, for example passing a `userId`, you can do that simply by modifying the query you sent to your `process` callback method. For example

```ts
// Web API call
getCustomerApiCall(odataQuery) { with Fetch Client
  const finalQuery = `${odataQuery}$filter=(userId eq 12345)`;
  return this.http.get(`/api/getCustomers?${finalQuery}`);
}
```

### OData options

All options can be found here: [Slickgrid-Universal - OData Options](https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/odata/src/interfaces/odataOption.interface.ts)

Some are described in more detail below.

#### OData version

By default the OData version is set to 2 because it was implemented with that version. If you wish to use version 4, then just change the `version: 4`, there are subtle differences.

```ts
this.gridOptions = {
  backendServiceApi: {
    service: new GridOdataService(),
      options: {
        enableCount: true, // add the count in the OData query, which will return a property named "odata.count" (v2) or "@odata.count" (v4)
        version: 4        // defaults to 2, the query string is slightly different between OData 2 and 4
      } as OdataOption,
      process: (query) => this.getCustomerApiCall(query),
      postProcess: (response) => {
        this.metrics = response.metrics;
        this.displaySpinner(false);
        this.getCustomerCallback(response);
      }
  } as OdataServiceApi
};
```

#### Query total items count

The total items count can be queried from the backend by:

```ts
const oDataOptions: OdataOption = {
  enableCount: true;
}
```

When enabled that will add `$inlinecount=allpages` (v2/v3) or `$count=true` (v4) to the query. And the count from the backend's response is extracted and `pagination.totalItems` is updated with that count. The property in the response that is used depends on the oData version specified: `d.__count` for v2, `__count` for v3 and `@odata.count` for v4. If needed a custom extractor function can be set through `oDataOptions.countExtractor`.

#### Query only the grid column's fields

Query only the grid column's fields from the backend by:

```ts
const oDataOptions: OdataOption = {
  enableSelect: true;
}
```

For example `columns: [{ id: 'col1', field: 'field1' }, { id: 'col2', field: 'field2' }]` results in the query: `?$select=id,field1,field2`.

A property `id` is always selected from the backend because the grid requires it. This property can be changed by setting `gridOptions.datasetIdPropertyName`.

#### Query related resources / expand navigation properties

Specify that related resources (navigation properties) should be retrieved from the backend:

```ts
const oDataOptions: OdataOption = {
  enableExpand: true;
}
```

A navigation property is identified as a field having `/` in it's name. For example `columns: [{ id: 'col1', field: 'nav1/field1' }, { id: 'col2', field: 'nav2/field1' }]` results in the query `?$expand=nav1,nav2`

Often `enableSelect` and `enableExpand` are used in conjunction. And with oData v4 then also navigation properties are selected from the backend. For example `columns: [{ id: 'col1', field: 'nav1/field1' }, { id: 'col2', field: 'nav2/field1' }]` results in the query `?$select=id,$expand=nav1($select=field1),nav2($select=field2)`

```ts
const oDataOptions: OdataOption = {
  enableSelect: true;
  enableExpand: true;
  version: 4
}
```

Navigations within navigations are also supported. For example `columns: [{ id: 'col1', field: 'nav1/subnav1/field1' }]`.

The dataset from the backend is automatically extracted and navigation fields are flattened so the grid can display them and sort/filter just work. The exact property that is used as the dataset depends on the oData version: `d.results` for v2, `results` for v3 and `value` for v4. If needed a custom extractor function can be set through `oDataOptions.datasetExtractor`. For example if the backend responds with `{ value: [{ id: 1, nav1: { field1: 'x' }, { nav2: { field2: 'y' } } ] }` this will be flattened to `{ value: [{ id: 1, 'nav1/field1': 'x', 'nav2/field2': 'y' } ] }`.

#### Override the filter query

Column filters may have a `Custom` operator, that acts as a placeholder for you to define your own logic. To do so, the easiest way is to provide the `filterQueryOverride` callback in the OdataOptions. This method will be called with `BackendServiceFilterQueryOverrideArgs` to let you decide dynamically on how the filter should be assembled.

E.g. you could listen for a specific column and the active 'Custom' in order to switch the filter to a matchesPattern SQL LIKE search:

```ts
backendServiceApi: {
  options: {
    filterQueryOverride: ({ fieldName, columnDef, columnFilterOperator, searchValues }) => {
      if (columnFilterOperator === 'Custom' && columnDef?.id === 'name') {
        let matchesSearch = searchValues[0].replace(/\*/g, '.*');
        matchesSearch = matchesSearch.slice(0, 1) + '%5E' + matchesSearch.slice(1);
        matchesSearch = matchesSearch.slice(0, -1) + '$\'';

        return `matchesPattern(${fieldName}, ${matchesSearch})`;
      }
    },
  }
}

```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ghiscoding.gitbook.io/slickgrid-universal/backend-services/odata.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
