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-datasourceid="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-datasourcecontroller="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 usethis.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 instancequery: 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 filterif (!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 instanceitems: 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 fieldsitems.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 objectdatasource: The data source instancequery: The query that failed
Example:
class DataController {onLoadDataFailed(error, datasource, query) {console.error('Failed to load data:', error);// Show user-friendly messagethis.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 instanceitems: Array of items to be saved
Example:
class DataController {onBeforeSaveData({datasource, items}) {console.log('About to save items:', items);// Validate all itemsfor (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 instanceitems: Array of items that were saved
Example:
class DataController {async onAfterSaveData({datasource, items}) {console.log('Successfully saved items:', items);// Show success messagethis.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 instanceitem: The item that changedfield: The field name that changedoldValue: The previous valuenewValue: The new value
Example:
class DataController {onValueChanged({datasource, item, field, oldValue, newValue}) {console.log(`${field} changed from ${oldValue} to ${newValue}`);// Dependent field logicif (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-datasourceid="employeeDS"controller="EmployeeController"src="employees.js"pre-load="true"/></datasources>
Controller Implementation
class EmployeeController {constructor() {this.retryCount = 0;this.maxRetries = 3;}// 1. InitializationonDatasourceInitialized(datasource) {this.datasource = datasource;
Key Differences: json-datasource vs maximo-datasource
| Feature | json-datasource | maximo-datasource |
|---|---|---|
| Data Source | REST APIs, local files, custom loaders | Maximo OSLC APIs |
| Data Location | Client-side (in-memory) | Server-side (Maximo database) |
| Schema | Optional, inferred from data | Required, defines Maximo attributes |
| Filtering | Client-side only | Server-side (where clause) + client-side |
| Security | Client-side only | Maximo security enforced |
| Relationships | Manual implementation | Built-in (object structure relationships) |
| Offline Support | Requires custom implementation | Built-in mobile sync |
| Best For | Non-Maximo data, prototypes | Maximo 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?
Asynchronous Data Loading: Loader functions are async, allowing you to fetch data from APIs, databases, or any asynchronous source without blocking the UI.
Dynamic Data: Unlike static arrays, loader functions can fetch fresh data each time the datasource loads, ensuring your UI always displays current information.
Query Parameter Support: The loader receives a
queryparameter, allowing you to implement filtering, sorting, and pagination based on user interactions.Error Handling: You can implement proper error handling and retry logic within the loader function.
Reusability: Loader functions can be shared across multiple datasources or applications.
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 loadsconsole.log('Loading with query:', query);try {// Fetch data from your APIconst response = await fetch('/api/products');
Advanced Loader Examples:
1. Loader with Query Parameters:
const loader = async (query) => {// Build URL with query parametersconst 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 storageconst 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 returningconst transformedItems = data.products.map(product => ({...product,// Add calculated fieldsdisplayPrice: `$${product.price.toFixed(2)}`,
4. Loader with Multiple Data Sources:
const loader = async (query) => {// Fetch from multiple endpoints in parallelconst [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 delayawait new Promise(resolve => setTimeout(resolve, 500));// Return mock datareturn {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-datasourceid="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:
Loading Data:
json-datasource: Uses loader function or static datamaximo-datasource: Queries Maximo using object-structure and where clause
Schema Definition:
json-datasource: Schema is optional, mainly for validationmaximo-datasource: Schema is required, maps to Maximo attributes
Filtering:
json-datasource: All filtering happens client-side after loadmaximo-datasource: Server-side filtering viawhereclause, plus client-side
Relationships:
json-datasource: Must manually load related datamaximo-datasource: Can use relationship paths (e.g.,asset.assetnum)
Common Data Source Operations
Accessing a Data Source
// In a controllerconst ds = this.app.findDatasource('myDS');// In a page controllerconst ds = this.page.findDatasource('myDS');
Loading Data
// Force reload from sourceawait ds.forceReload();// Load with custom queryawait ds.load({where: 'status="ACTIVE"',orderBy: 'name'});
Searching
// Simple searchds.search('John', ['firstName', 'lastName']);// QBE (Query By Example) searchds.searchQBE({age: '>30',department: 'Engineering'});
Iterating Items
// forEachds.forEach(item => {console.log(item.name);});// Get specific itemconst 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 itemds.undoItemChanges(item);// Undo all changesds.undoChanges();
Filtering
// Apply in-memory filterds.applyInMemoryFilter(item => {return item.status === 'ACTIVE' && item.priority > 5;});// Clear filterds.clearInMemoryFilter();
Best Practices
✅ Do’s
Use async loader functions for
json-datasourceconst loader = async (query) => {const response = await fetch('/api/data');return { items: await response.json() };};export default loader;Use
maximo-datasourcefor Maximo data<maximo-datasource object-structure="MXAPIWODETAIL" ...>Use
smart-inputfor editable forms- It automatically determines the correct input type from schema
Transform data in
onAfterLoadData- This is the right place to enrich or modify loaded data
Validate in
onBeforeSaveData- Prevent invalid data from being saved
Use
depends-onfor load order<json-datasource id="ds2" depends-on="ds1"/>
❌ Don’ts
Don’t use static arrays
// ❌ Badds.load({src: [{id: 1}, {id: 2}]});// ✅ Good - use loader functionDon’t create controllers unnecessarily
- Only create controllers when you need custom logic
Don’t use basic inputs for datasource forms
<!-- ❌ Bad --><text-input value="{item.name}"/><!-- ✅ Good --><smart-input attribute="name" datasource="myDS"/>Don’t forget error handling
- Always implement
onLoadDataFailed
- Always implement
Don’t modify items outside lifecycle events
- Use
onAfterLoadDataoronValueChanged
- Use
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 itemconst childDS = await parentDS.getChildDatasource('doclinks',item,{query: {attachment: true}});
Conditional Loading
Prevent loading until conditions are met:
<json-datasourceid="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:
- Choose the right type: Use
json-datasourcefor non-Maximo data,maximo-datasourcefor Maximo data - Leverage lifecycle events: Use controllers to customize behavior at key points
- Follow best practices: Use async loaders, validate before save, transform after load
- 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.