Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 87 additions & 8 deletions src/cohort-builder.utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { showToast } from '@openmrs/esm-framework';
import type { Column, Patient, Query } from './types';

export const composeJson = (searchParameters) => {
Expand Down Expand Up @@ -106,16 +107,94 @@ export const addColumnsToDisplay = () => {
return columnValues;
};

export const addToHistory = (description: string, patients: Patient[], parameters: {}) => {
const oldHistory = JSON.parse(window.sessionStorage.getItem('openmrsHistory'));
let newHistory = [];
const STORAGE_KEY = 'openmrsHistory';
const MAX_HISTORY_ITEMS = 50; // LRU cache: Keep only most recent 50 searches
const MAX_PATIENTS_PER_SEARCH = 100; // Limit patient data to prevent storage bloat

if (oldHistory) {
newHistory = [...oldHistory, { description, patients, parameters }];
} else {
newHistory = [{ description, patients, parameters }];
/**
* Safely adds search to history with comprehensive error handling
* Implements LRU cache, quota management, and graceful degradation
* @param description - Human-readable description of the search
* @param patients - Array of patient results
* @param parameters - Search parameters used
* @returns boolean - true if successfully saved, false otherwise
*/
export const addToHistory = (description: string, patients: Patient[], parameters: {}): boolean => {
try {
// Validate inputs
if (!Array.isArray(patients) || typeof parameters !== 'object') {
return false;
}

// Limit patient data to prevent storage bloat
const limitedPatients = patients.slice(0, MAX_PATIENTS_PER_SEARCH);

// Safely retrieve existing history
let oldHistory: any[] = [];
const storedData = window.sessionStorage.getItem(STORAGE_KEY);

if (storedData) {
try {
const parsed = JSON.parse(storedData);
oldHistory = Array.isArray(parsed) ? parsed : [];
} catch (parseError) {
// Reset corrupted data
window.sessionStorage.removeItem(STORAGE_KEY);
oldHistory = [];
}
}

// Add new entry with timestamp
const newEntry = {
description,
patients: limitedPatients,
parameters,
timestamp: new Date().toISOString(),
};

// Implement LRU: Keep only most recent items
const newHistory = [...oldHistory, newEntry].slice(-MAX_HISTORY_ITEMS);

// Try to save with quota handling
try {
const serialized = JSON.stringify(newHistory);

window.sessionStorage.setItem(STORAGE_KEY, serialized);
return true;
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
// Fallback: Keep only last 10 items
const reducedHistory = newHistory.slice(-10);
try {
window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(reducedHistory));
showToast({
title: 'Search History Limit Reached',
kind: 'warning',
description: 'Older search history has been cleared to save space.',
});
return true;
} catch (retryError) {
// Complete failure - disable history
window.sessionStorage.removeItem(STORAGE_KEY);
showToast({
title: 'Search History Unavailable',
kind: 'error',
description: 'Unable to save search history. Your searches will still work.',
});
return false;
}
}
throw storageError; // Re-throw unexpected errors
}
} catch (error) {
// Don't break searching if history fails
showToast({
title: 'History Error',
kind: 'error',
description: 'Could not save search to history, but your search completed successfully.',
});
return false;
}
window.sessionStorage.setItem('openmrsHistory', JSON.stringify(newHistory));
};

export const formatDate = (dateString: string) => {
Expand Down
37 changes: 27 additions & 10 deletions src/components/composition/composition.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,33 @@ export const createCompositionQuery = (compositionQuery: string) => {

searchTokens.forEach((eachToken) => {
if (eachToken.match(/\d/)) {
const history = JSON.parse(window.sessionStorage.getItem('openmrsHistory'));
const operandQuery = history[parseInt(eachToken) - 1];

const jsonRequestObject = operandQuery.parameters;
jsonRequestObject.customRowFilterCombination = formatFilterCombination(
jsonRequestObject.customRowFilterCombination,
query.rowFilters.length,
);
query.customRowFilterCombination += `(${jsonRequestObject.customRowFilterCombination})`;
query.rowFilters = query.rowFilters.concat(jsonRequestObject.rowFilters);
try {
const storedData = window.sessionStorage.getItem('openmrsHistory');
if (!storedData) {
return;
}

const history = JSON.parse(storedData);
if (!Array.isArray(history)) {
return;
}

const operandQuery = history[parseInt(eachToken) - 1];
if (!operandQuery?.parameters) {
return;
}

const jsonRequestObject = operandQuery.parameters;
jsonRequestObject.customRowFilterCombination = formatFilterCombination(
jsonRequestObject.customRowFilterCombination,
query.rowFilters.length,
);
query.customRowFilterCombination += `(${jsonRequestObject.customRowFilterCombination})`;
query.rowFilters = query.rowFilters.concat(jsonRequestObject.rowFilters);
} catch (error) {
// Skip invalid history entry
return;
}
} else {
query.customRowFilterCombination += ` ${eachToken} `;
}
Expand Down
56 changes: 45 additions & 11 deletions src/components/search-history/search-history.utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
import { type SearchHistoryItem } from '../../types';

export const getSearchHistory = () => {
const history = JSON.parse(window.sessionStorage.getItem('openmrsHistory'));
const searchHistory: SearchHistoryItem[] = [];
history?.map((historyItem, index) =>
searchHistory.push({
...historyItem,
id: (index + 1).toString(),
results: historyItem.patients.length,
}),
);
return searchHistory;
const STORAGE_KEY = 'openmrsHistory';

/**
* Safely retrieves search history from sessionStorage with error handling
* @returns Array of search history items, or empty array if data is corrupted/unavailable
*/
export const getSearchHistory = (): SearchHistoryItem[] => {
try {
const storedData = window.sessionStorage.getItem(STORAGE_KEY);

if (!storedData) {
return [];
}

const history = JSON.parse(storedData);

// Validate that parsed data is an array
if (!Array.isArray(history)) {
window.sessionStorage.removeItem(STORAGE_KEY);
return [];
}

const searchHistory: SearchHistoryItem[] = [];

history.forEach((historyItem, index) => {
// Validate each history item has required properties
if (historyItem && historyItem.patients && Array.isArray(historyItem.patients)) {
searchHistory.push({
...historyItem,
id: (index + 1).toString(),
results: historyItem.patients.length,
});
}
});

return searchHistory;
} catch (error) {
// Clear corrupted data to prevent repeated errors
try {
window.sessionStorage.removeItem(STORAGE_KEY);
} catch (removeError) {
// Silent failure - storage access denied
}
return [];
}
};