Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: sample queries cache #2239

Merged
merged 7 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
18 changes: 15 additions & 3 deletions src/app/middleware/localStorageMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { samplesCache } from '../../modules/cache/samples.cache';
import { saveTheme } from '../../themes/theme-utils';
import { AppAction } from '../../types/action';
import { CHANGE_THEME_SUCCESS } from '../services/redux-constants';
import {
CHANGE_THEME_SUCCESS, SAMPLES_FETCH_SUCCESS
} from '../services/redux-constants';

const localStorageMiddleware = () => (next: any) => (action: AppAction) => {
if (action.type === CHANGE_THEME_SUCCESS) {
saveTheme(action.response);
switch (action.type) {
case CHANGE_THEME_SUCCESS:
saveTheme(action.response);
break;

case SAMPLES_FETCH_SUCCESS:
samplesCache.saveSamples(action.response);
break;

default:
break;
}
return next(action);
};
Expand Down
4 changes: 2 additions & 2 deletions src/app/services/actions/query-action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IQuery } from '../../../types/query-runner';
import { IStatus } from '../../../types/status';
import { ClientError } from '../../utils/error-utils/ClientError';
import { setStatusMessage } from '../../utils/status-message';
import { writeHistoryData } from '../../views/sidebar/history/history-utils';
import { historyCache } from '../../../modules/cache/history-utils';
import {
anonymousRequest,
authenticatedRequest,
Expand Down Expand Up @@ -180,7 +180,7 @@ async function createHistory(
result
};

writeHistoryData(historyItem);
historyCache.writeHistoryData(historyItem);

dispatch(addHistoryItem(historyItem));
return result;
Expand Down
6 changes: 3 additions & 3 deletions src/app/services/actions/request-history-action-creators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import { AppAction } from '../../../types/action';
import { IHistoryItem } from '../../../types/history';
import { bulkRemoveHistoryData, removeHistoryData } from '../../views/sidebar/history/history-utils';
import { historyCache } from '../../../modules/cache/history-utils';
import {
ADD_HISTORY_ITEM_SUCCESS,
REMOVE_ALL_HISTORY_ITEMS_SUCCESS,
Expand Down Expand Up @@ -35,7 +35,7 @@ export function removeHistoryItem(historyItem: IHistoryItem) {

delete historyItem.category;
return async (dispatch: Function) => {
return removeHistoryData(historyItem)
return historyCache.removeHistoryData(historyItem)
.then(() => {
dispatch({
type: REMOVE_HISTORY_ITEM_SUCCESS,
Expand All @@ -53,7 +53,7 @@ export function bulkRemoveHistoryItems(historyItems: IHistoryItem[]) {
});

return async (dispatch: Function) => {
return bulkRemoveHistoryData(listOfKeys)
return historyCache.bulkRemoveHistoryData(listOfKeys)
.then(() => {
dispatch({
type: REMOVE_ALL_HISTORY_ITEMS_SUCCESS,
Expand Down
8 changes: 7 additions & 1 deletion src/app/services/actions/samples-action-creators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { geLocale } from '../../../appLocale';
import { samplesCache } from '../../../modules/cache/samples.cache';
import { AppDispatch } from '../../../store';
import { AppAction } from '../../../types/action';
import { IRequestOptions } from '../../../types/request';
import { queries } from '../../views/sidebar/sample-queries/queries';
import {
SAMPLES_FETCH_ERROR,
SAMPLES_FETCH_PENDING,
Expand Down Expand Up @@ -55,7 +57,11 @@ export function fetchSamples() {
const res = await response.json();
return dispatch(fetchSamplesSuccess(res.sampleQueries));
} catch (error) {
return dispatch(fetchSamplesError({ error }));
let cachedSamples = await samplesCache.readSamples();
if (cachedSamples.length === 0) {
cachedSamples = queries;
}
return dispatch(fetchSamplesError(cachedSamples));
}
};
}
8 changes: 2 additions & 6 deletions src/app/services/reducers/samples-reducers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,11 @@ describe('Samples Reducer', () => {
error: null
};

const mockResponse = {
status: 400
};

const newState = { ...initialState };
newState.error = mockResponse;
newState.error = 'error';
newState.queries = queries;

const queryAction = { type: SAMPLES_FETCH_ERROR, response: mockResponse };
const queryAction = { type: SAMPLES_FETCH_ERROR, response: queries };
const state = samples(initialState, queryAction);

expect(state).toEqual(newState);
Expand Down
5 changes: 2 additions & 3 deletions src/app/services/reducers/samples-reducers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AppAction } from '../../../types/action';
import { queries } from '../../views/sidebar/sample-queries/queries';
import { SAMPLES_FETCH_ERROR, SAMPLES_FETCH_PENDING, SAMPLES_FETCH_SUCCESS } from '../redux-constants';

const initialState = {
Expand All @@ -25,8 +24,8 @@ export function samples(state = initialState, action: AppAction): any {
return {
...state,
pending: false,
queries,
error: action.response
queries: action.response,
error: 'error'
};
default:
return state;
Expand Down
6 changes: 5 additions & 1 deletion src/app/views/sidebar/history/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DialogFooter, DialogType, getId, getTheme, IColumn, IconButton,
Label, MessageBar, MessageBarType, PrimaryButton, SearchBox, SelectionMode, styled, TooltipHost
} from '@fluentui/react';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { useDispatch } from 'react-redux';

Expand Down Expand Up @@ -94,6 +94,10 @@ const History = (props: any) => {

const classes = classNames(props);

useEffect(() => {
setHistoryItems(history);
}, [history])

if (!history || history.length === 0) {
return NoResultsFound('We did not find any history items');
}
Expand Down
49 changes: 0 additions & 49 deletions src/app/views/sidebar/history/history-utils.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { bulkAddHistoryItems } from './app/services/actions/request-history-acti
import { changeThemeSuccess } from './app/services/actions/theme-action-creator';
import { isValidHttpsUrl } from './app/utils/external-link-validation';
import App from './app/views/App';
import { readHistoryData } from './app/views/sidebar/history/history-utils';
import { historyCache } from './modules/cache/history-utils';
import { geLocale } from './appLocale';
import messages from './messages';
import { authenticationWrapper } from './modules/authentication';
Expand Down Expand Up @@ -137,7 +137,7 @@ if (devxApiUrl && isValidHttpsUrl(devxApiUrl)) {
appStore.dispatch(setDevxApiUrl(devxApi));
}

readHistoryData().then((data: any) => {
historyCache.readHistoryData().then((data: any) => {
if (data.length > 0) {
appStore.dispatch(bulkAddHistoryItems(data));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IHistoryItem } from '../../../../types/history';
import { writeHistoryData, readHistoryData, removeHistoryData } from './history-utils';
import { IHistoryItem } from '../../types/history';
import { historyCache } from './history-utils';


let historyItems: IHistoryItem[] = [];
Expand Down Expand Up @@ -43,8 +43,8 @@ describe('History utils should', () => {
status: 200
}
expect(historyItems.length).toBe(0);
await writeHistoryData(historyItem);
const historyData = await readHistoryData();
await historyCache.writeHistoryData(historyItem);
const historyData = await historyCache.readHistoryData();
expect(historyData.length).toBe(1);
});

Expand All @@ -61,9 +61,9 @@ describe('History utils should', () => {
duration: 200,
status: 200
}
await writeHistoryData(historyItem);
await historyCache.writeHistoryData(historyItem);
expect(historyItems.length).toBe(2);
await removeHistoryData(historyItem);
await historyCache.removeHistoryData(historyItem);
expect(historyItems.length).toBe(1);
});

Expand All @@ -83,7 +83,7 @@ describe('History utils should', () => {
}
historyItems.push(historyItem);
expect(historyItems.length).toBe(1);
const historyData = await readHistoryData();
const historyData = await historyCache.readHistoryData();
expect(historyData.length).toBe(0);
});
})
59 changes: 59 additions & 0 deletions src/modules/cache/history-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import localforage from 'localforage';
import { IHistoryItem } from '../../types/history';

const historyStorage = localforage.createInstance({
storeName: 'history',
name: 'GE_V4'
});

function historyItemHasExpired(createdAt: string): boolean {
const ageInDays: number = 30;
const dateToCompare = new Date();
dateToCompare.setDate(dateToCompare.getDate() - ageInDays);
const expiryDate = dateToCompare.getTime();
const createdTime = new Date(createdAt).getTime();
return (createdTime < expiryDate);
}

export const historyCache = (function () {

const writeHistoryData = (historyItem: IHistoryItem) => {
historyStorage.setItem(historyItem.createdAt, historyItem);
}

const readHistoryData = async (): Promise<IHistoryItem[]> => {
let historyData: IHistoryItem[] = [];
const keys = await historyStorage.keys();
for (const creationTime of keys) {
if (historyItemHasExpired(creationTime)) {
historyStorage.removeItem(creationTime);
} else {
const historyItem = await historyStorage.getItem(creationTime)! as IHistoryItem;
historyData = [...historyData, historyItem];
}
}
return historyData;
}

const removeHistoryData = async (historyItem: IHistoryItem) => {
await historyStorage.removeItem(historyItem.createdAt);
return true;
};

const bulkRemoveHistoryData = async (listOfKeys: string[]) => {
historyStorage.iterate((_value, key) => {
if (listOfKeys.includes(key)) {
historyStorage.removeItem(key);
}
}).then(() => {
return true;
});
};

return {
writeHistoryData,
bulkRemoveHistoryData,
removeHistoryData,
readHistoryData
}
})();
33 changes: 33 additions & 0 deletions src/modules/cache/samples.cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ISampleQuery } from '../../types/query-runner';
import { samplesCache } from './samples.cache';

jest.mock('localforage', () => ({
// eslint-disable-next-line @typescript-eslint/no-empty-function
config: () => { },
createInstance: () => ({
// eslint-disable-next-line @typescript-eslint/no-empty-function
getItem: () => { },
// eslint-disable-next-line @typescript-eslint/no-empty-function
setItem: () => { }
})
}));

const queries: ISampleQuery[] = [];
describe('Samples Cache should', () => {
it('return the same queries after queries added', async () => {
expect(queries.length).toBe(0);
queries.push(
{
category: 'Getting Started',
method: 'GET',
humanName: 'my profile',
requestUrl: '/v1.0/me',
docLink: 'https://learn.microsoft.com/en-us/graph/api/user-get',
skipTest: false
},
);
samplesCache.saveSamples(queries);
const samplesData = await samplesCache.readSamples();
expect(samplesData).not.toBe(queries);
})
})
29 changes: 29 additions & 0 deletions src/modules/cache/samples.cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import localforage from 'localforage';
import { ISampleQuery } from '../../types/query-runner';

const samplesStorage = localforage.createInstance({
storeName: 'samples',
name: 'GE_V4'
});

const SAMPLE_KEY = 'sample-queries';

export const samplesCache = (function () {

const saveSamples = async (queries: ISampleQuery[]) => {
await samplesStorage.setItem(SAMPLE_KEY, JSON.stringify(queries));
}

const readSamples = async (): Promise<ISampleQuery[]> => {
const items = await samplesStorage.getItem(SAMPLE_KEY) as string;
if (items) {
return JSON.parse(items);
}
return [];
}

return {
saveSamples,
readSamples
}
})();