Skip to main contentMAF Configuration Practices

Graphite Data Sources - Comprehensive Guide

What is a Data Source?

A data source in Graphite is a component that provides data to UI components. It acts as a bridge between your data (whether from APIs, local files, or Maximo servers) and the visual elements that display that data. Data sources handle all the complex operations of loading, caching, filtering, searching, paginating, and modifying data, so you can focus on building your UI.

Think of a data source as a smart data container that:

  • Loads and caches records from various sources
  • Searches, filters, and paginates data efficiently
  • Saves, adds, updates, and deletes data with built-in change tracking
  • Notifies UI components when data changes
  • Manages data state and synchronization

Key Benefits

  • Separation of Concerns: Data logic is separated from UI logic
  • Automatic UI Updates: When data changes, connected UI components automatically update
  • Built-in Operations: Common data operations (CRUD) are handled for you
  • Performance: Intelligent caching and loading strategies
  • Consistency: Standardized way to work with data across your application

Types of Data Sources

Graphite provides three types of data sources, each suited for different use cases:

1. <json-datasource>

Purpose: Client-side, in-memory data source for non-Maximo data.

Best for:

  • Loading data from REST APIs
  • Working with local JSON files
  • Client-side data manipulation
  • Testing and prototyping
  • Data that doesn’t come from Maximo

Example:

<json-datasource id="itemsDS" src="loader.js" pre-load="true" />

2. <maximo-datasource>

Purpose: Connects directly to Maximo OSLC APIs for server-side data.

Best for:

  • Loading Maximo business objects (Work Orders, Assets, etc.)
  • Server-side filtering and querying
  • Working with Maximo object structures
  • Enterprise data that requires security and validation
  • Data that needs to be synchronized with Maximo

Example:

<maximo-datasource
id="workOrderDS"
object-structure="MXAPIWODETAIL"
where="status='WAPPR'">
<schema>
<attribute name="wonum"/>
<attribute name="description"/>
<attribute name="status"/>
</schema>

3. <custom-datasource>

Purpose: Fully custom data adapter for specialized requirements.

Best for:

  • Complex data operations not covered by json or maximo datasources
  • Custom data sources with unique protocols
  • Advanced scenarios requiring full control

Note: This is rarely needed. Most use cases are covered by json-datasource or maximo-datasource.


Data Source Lifecycle Events

Data sources provide lifecycle hooks that allow you to execute custom logic at specific points in the data source’s lifecycle. These events are defined in a controller attached to the data source.

Attaching a Controller

Controllers are JavaScript classes that handle data source events and custom logic:

<json-datasource
controller="DataController"
id="myDS"
src="data.js"/>

The controller file (DataController.js):

class DataController {
// Lifecycle events go here
}
export default DataController;

Core Lifecycle Events

1. onDatasourceInitialized(datasource)

When: Called once when the data source is first created and initialized.

Purpose:

  • Store reference to the data source
  • Perform one-time setup
  • Initialize state or variables

Example:

class DataController {
onDatasourceInitialized(datasource) {
this.datasource = datasource;
console.log('Datasource initialized:', datasource.id);
// Store reference to app for later use
this.app = this.datasource.app;
}
}

2. onBeforeLoadData(datasource, query)

When: Called before data is loaded from the source.

Purpose:

  • Modify the query before it’s sent
  • Add filters or parameters
  • Validate load conditions
  • Cancel the load by returning false

Parameters:

  • datasource: The data source instance
  • query: The query object that will be used to load data

Example:

class DataController {
onBeforeLoadData(datasource, query) {
console.log('About to load data with query:', query);
// Add a custom filter
if (!query.where) {
query.where = '';
}
query.where += ' and status="ACTIVE"';

3. onAfterLoadData(datasource, items, query)

When: Called after data is loaded but before it’s merged into the data source.

Purpose:

  • Transform or enrich loaded data
  • Add calculated fields
  • Filter items client-side
  • Modify items before they’re displayed

Parameters:

  • datasource: The data source instance
  • items: Array of loaded items (can be modified)
  • query: The query that was used to load data

Important: This is your chance to mutate items before they become part of the data source state.

Example:

class DataController {
onAfterLoadData(datasource, items, query) {
console.log(`Loaded ${items.length} items`);
// Add calculated fields
items.forEach(item => {
item.fullName = `${item.firstName} ${item.lastName}`;
item.isUrgent = item.priority > 8;

4. onLoadDataFailed(error, datasource, query)

When: Called when data loading fails.

Purpose:

  • Handle errors gracefully
  • Show user-friendly error messages
  • Log errors for debugging
  • Implement retry logic

Parameters:

  • error: The error object
  • datasource: The data source instance
  • query: The query that failed

Example:

class DataController {
onLoadDataFailed(error, datasource, query) {
console.error('Failed to load data:', error);
// Show user-friendly message
this.app.toast(
'Unable to load data. Please try again.',
'error'
);

5. onBeforeSaveData({datasource, items})

When: Called before modified items are saved.

Purpose:

  • Validate data before saving
  • Add additional fields
  • Transform data for the server
  • Cancel save by returning false

Parameters: Object containing:

  • datasource: The data source instance
  • items: Array of items to be saved

Example:

class DataController {
onBeforeSaveData({datasource, items}) {
console.log('About to save items:', items);
// Validate all items
for (let item of items) {
if (!item.description || item.description.trim() === '') {
this.app.toast('Description is required', 'error');
return false; // Cancel save

6. onAfterSaveData({datasource, items})

When: Called after items are successfully saved.

Purpose:

  • Show success messages
  • Trigger related actions
  • Update other data sources
  • Navigate to another page

Parameters: Object containing:

  • datasource: The data source instance
  • items: Array of items that were saved

Example:

class DataController {
async onAfterSaveData({datasource, items}) {
console.log('Successfully saved items:', items);
// Show success message
this.app.toast(
`${items.length} item(s) saved successfully`,
'success'
);

7. onValueChanged({datasource, item, field, oldValue, newValue})

When: Called whenever a field value changes in any item.

Purpose:

  • React to field changes
  • Implement dependent field logic
  • Validate changes in real-time
  • Update calculated fields

Parameters: Object containing:

  • datasource: The data source instance
  • item: The item that changed
  • field: The field name that changed
  • oldValue: The previous value
  • newValue: The new value

Example:

class DataController {
onValueChanged({datasource, item, field, oldValue, newValue}) {
console.log(`${field} changed from ${oldValue} to ${newValue}`);
// Dependent field logic
if (field === 'quantity') {
item.totalPrice = item.quantity * item.unitPrice;
}

Complete Lifecycle Example

Here’s a comprehensive example showing all lifecycle events working together:

XML Declaration

<application id="myApp">
<datasources>
<json-datasource
id="employeeDS"
controller="EmployeeController"
src="employees.js"
pre-load="true"/>
</datasources>

Controller Implementation

class EmployeeController {
constructor() {
this.retryCount = 0;
this.maxRetries = 3;
}
// 1. Initialization
onDatasourceInitialized(datasource) {
this.datasource = datasource;

Key Differences: json-datasource vs maximo-datasource

Featurejson-datasourcemaximo-datasource
Data SourceREST APIs, local files, custom loadersMaximo OSLC APIs
Data LocationClient-side (in-memory)Server-side (Maximo database)
SchemaOptional, inferred from dataRequired, defines Maximo attributes
FilteringClient-side onlyServer-side (where clause) + client-side
SecurityClient-side onlyMaximo security enforced
RelationshipsManual implementationBuilt-in (object structure relationships)
Offline SupportRequires custom implementationBuilt-in mobile sync
Best ForNon-Maximo data, prototypesMaximo business objects

json-datasource Example

<json-datasource id="productsDS" src="products.js" pre-load="true">
<schema>
<attribute name="id" type="INTEGER"/>
<attribute name="name" type="STRING"/>
<attribute name="price" type="DECIMAL"/>
</schema>
</json-datasource>

Understanding Loader Functions

A loader function is an async JavaScript function that fetches and returns data for a json-datasource. It’s the recommended way to load data because it provides flexibility, maintainability, and follows best practices.

Why Use a Loader Function?

  1. Asynchronous Data Loading: Loader functions are async, allowing you to fetch data from APIs, databases, or any asynchronous source without blocking the UI.

  2. Dynamic Data: Unlike static arrays, loader functions can fetch fresh data each time the datasource loads, ensuring your UI always displays current information.

  3. Query Parameter Support: The loader receives a query parameter, allowing you to implement filtering, sorting, and pagination based on user interactions.

  4. Error Handling: You can implement proper error handling and retry logic within the loader function.

  5. Reusability: Loader functions can be shared across multiple datasources or applications.

  6. Testability: Isolated loader functions are easier to unit test than inline data loading logic.

Loader file (products.js):

const loader = async (query) => {
// The query parameter contains any filters, search terms, or parameters
// passed when the datasource loads
console.log('Loading with query:', query);
try {
// Fetch data from your API
const response = await fetch('/api/products');

Advanced Loader Examples:

1. Loader with Query Parameters:

const loader = async (query) => {
// Build URL with query parameters
const params = new URLSearchParams();
if (query.search) {
params.append('search', query.search);
}
if (query.category) {

2. Loader with Authentication:

const loader = async (query) => {
// Get auth token from app state or storage
const token = localStorage.getItem('authToken');
const response = await fetch('/api/products', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}

3. Loader with Data Transformation:

const loader = async (query) => {
const response = await fetch('/api/products');
const data = await response.json();
// Transform data before returning
const transformedItems = data.products.map(product => ({
...product,
// Add calculated fields
displayPrice: `$${product.price.toFixed(2)}`,

4. Loader with Multiple Data Sources:

const loader = async (query) => {
// Fetch from multiple endpoints in parallel
const [productsRes, categoriesRes] = await Promise.all([
fetch('/api/products'),
fetch('/api/categories')
]);
const products = await productsRes.json();
const categories = await categoriesRes.json();

5. Loader with Local Data (for testing):

const loader = async (query) => {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
// Return mock data
return {
items: [
{ id: 1, name: 'Product 1', price: 29.99 },
{ id: 2, name: 'Product 2', price: 39.99 },

When NOT to Use a Loader Function:

  • When you have truly static data that never changes
  • For very simple prototypes where you just need placeholder data
  • When the data is already available in the application state

However, even in these cases, using a loader function is still recommended for consistency and future flexibility.

maximo-datasource Example

<maximo-datasource
id="workOrderDS"
object-structure="MXAPIWODETAIL"
where="status in ['WAPPR','INPRG']"
order-by="wonum desc">
<schema>
<attribute name="wonum"/>
<attribute name="description"/>
<attribute name="status"/>

Key Differences in Usage:

  1. Loading Data:

    • json-datasource: Uses loader function or static data
    • maximo-datasource: Queries Maximo using object-structure and where clause
  2. Schema Definition:

    • json-datasource: Schema is optional, mainly for validation
    • maximo-datasource: Schema is required, maps to Maximo attributes
  3. Filtering:

    • json-datasource: All filtering happens client-side after load
    • maximo-datasource: Server-side filtering via where clause, plus client-side
  4. Relationships:

    • json-datasource: Must manually load related data
    • maximo-datasource: Can use relationship paths (e.g., asset.assetnum)

Common Data Source Operations

Accessing a Data Source

// In a controller
const ds = this.app.findDatasource('myDS');
// In a page controller
const ds = this.page.findDatasource('myDS');

Loading Data

// Force reload from source
await ds.forceReload();
// Load with custom query
await ds.load({
where: 'status="ACTIVE"',
orderBy: 'name'
});

Searching

// Simple search
ds.search('John', ['firstName', 'lastName']);
// QBE (Query By Example) search
ds.searchQBE({
age: '>30',
department: 'Engineering'
});

Iterating Items

// forEach
ds.forEach(item => {
console.log(item.name);
});
// Get specific item
const firstItem = ds.get(0);
const itemById = ds.getById('123');

CRUD Operations

Create

let newItem = await ds.addNew();
newItem.name = 'New Item';
newItem.status = 'ACTIVE';
await ds.save();

Update

let item = ds.get(0);
item.description = 'Updated description';
await ds.save();

Delete

let item = ds.get(0);
let deleted = await ds.deleteItem(item);
if (deleted) {
await ds.load(); // Reload to reflect changes
}

Undo Changes

// Undo changes to a specific item
ds.undoItemChanges(item);
// Undo all changes
ds.undoChanges();

Filtering

// Apply in-memory filter
ds.applyInMemoryFilter(item => {
return item.status === 'ACTIVE' && item.priority > 5;
});
// Clear filter
ds.clearInMemoryFilter();

Best Practices

✅ Do’s

  1. Use async loader functions for json-datasource

    const loader = async (query) => {
    const response = await fetch('/api/data');
    return { items: await response.json() };
    };
    export default loader;
  2. Use maximo-datasource for Maximo data

    <maximo-datasource object-structure="MXAPIWODETAIL" ...>
  3. Use smart-input for editable forms

    • It automatically determines the correct input type from schema
  4. Transform data in onAfterLoadData

    • This is the right place to enrich or modify loaded data
  5. Validate in onBeforeSaveData

    • Prevent invalid data from being saved
  6. Use depends-on for load order

    <json-datasource id="ds2" depends-on="ds1"/>

❌ Don’ts

  1. Don’t use static arrays

    // ❌ Bad
    ds.load({src: [{id: 1}, {id: 2}]});
    // ✅ Good - use loader function
  2. Don’t create controllers unnecessarily

    • Only create controllers when you need custom logic
  3. Don’t use basic inputs for datasource forms

    <!-- ❌ Bad -->
    <text-input value="{item.name}"/>
    <!-- ✅ Good -->
    <smart-input attribute="name" datasource="myDS"/>
  4. Don’t forget error handling

  5. Don’t modify items outside lifecycle events


Advanced Topics

Depends-On Relationships

Control the order in which data sources load:

<json-datasource id="departments" src="dept.js"/>
<json-datasource id="employees" src="emp.js" depends-on="departments"/>

The employees datasource will wait until departments has loaded.

Child Datasources

For nested data relationships:

// Get child datasource for a specific item
const childDS = await parentDS.getChildDatasource(
'doclinks',
item,
{query: {attachment: true}}
);

Conditional Loading

Prevent loading until conditions are met:

<json-datasource
id="myDS"
src="data.js"
can-load="{app.state.userAuthenticated}"/>

Pre-loading

Load data immediately when the datasource is created:

<json-datasource id="myDS" src="data.js" pre-load="true"/>

Summary

Data sources are the backbone of Graphite applications, providing a consistent and powerful way to manage data. Key takeaways:

  1. Choose the right type: Use json-datasource for non-Maximo data, maximo-datasource for Maximo data
  2. Leverage lifecycle events: Use controllers to customize behavior at key points
  3. Follow best practices: Use async loaders, validate before save, transform after load
  4. Keep it simple: Only add controllers when you need custom logic

By understanding data sources and their lifecycle, you can build robust, maintainable Graphite applications that efficiently manage data from any source.

Page last updated: 05 November 2025