From ab14835dd60270d67d0d3cf56f8c3a57d70d2ca1 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 30 Sep 2024 15:18:28 +0300 Subject: [PATCH 1/4] Add odata abnf slice that makes a network call to get the rules --- .../autocomplete-action-creators.spec.ts | 3 +- .../permissions-action-creator.spec.ts | 3 +- .../resource-explorer-action-creators.spec.ts | 3 +- src/app/services/graph-constants.ts | 4 +- src/app/services/reducers/index.ts | 4 +- src/app/services/slices/odataabnf.slice.ts | 40 +++++++++++++++++++ 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/app/services/slices/odataabnf.slice.ts diff --git a/src/app/services/actions/autocomplete-action-creators.spec.ts b/src/app/services/actions/autocomplete-action-creators.spec.ts index e9b932fafa..38142adf4f 100644 --- a/src/app/services/actions/autocomplete-action-creators.spec.ts +++ b/src/app/services/actions/autocomplete-action-creators.spec.ts @@ -104,7 +104,8 @@ const mockState: ApplicationState = { error: null }, collections: [], - proxyUrl: '' + proxyUrl: '', + odataAbnf: '' } store.getState = () => ({ diff --git a/src/app/services/actions/permissions-action-creator.spec.ts b/src/app/services/actions/permissions-action-creator.spec.ts index 999336a6b6..614e9540c9 100644 --- a/src/app/services/actions/permissions-action-creator.spec.ts +++ b/src/app/services/actions/permissions-action-creator.spec.ts @@ -125,7 +125,8 @@ const mockState: ApplicationState = { error: null }, collections: [], - proxyUrl: '' + proxyUrl: '', + odataAbnf: '' } const currentState = store.getState(); store.getState = () => { diff --git a/src/app/services/actions/resource-explorer-action-creators.spec.ts b/src/app/services/actions/resource-explorer-action-creators.spec.ts index edba19e182..cc210e7aa8 100644 --- a/src/app/services/actions/resource-explorer-action-creators.spec.ts +++ b/src/app/services/actions/resource-explorer-action-creators.spec.ts @@ -100,7 +100,8 @@ const mockState: ApplicationState = { error: null }, collections: [], - proxyUrl: '' + proxyUrl: '', + odataAbnf: '' } const paths = [ diff --git a/src/app/services/graph-constants.ts b/src/app/services/graph-constants.ts index bdc5315221..1f09c5d605 100644 --- a/src/app/services/graph-constants.ts +++ b/src/app/services/graph-constants.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ export const GRAPH_URL = 'https://graph.microsoft.com'; export const GRAPH_API_VERSIONS = ['v1.0', 'beta']; export const USER_INFO_URL = `${GRAPH_URL}/v1.0/me`; @@ -32,4 +33,5 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s // eslint-disable-next-line max-len export const CONSENT_TYPE_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#:~:text=(eq%20only).-,consentType,-String' export const CURRENT_THEME='CURRENT_THEME'; -export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas' \ No newline at end of file +export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas' +export const ODATA_ABNF_CONSTRUCTION_RULES_ENDPOINT = 'https://raw.githubusercontent.com/oasis-tcs/odata-abnf/refs/heads/main/abnf/odata-abnf-construction-rules.txt' \ No newline at end of file diff --git a/src/app/services/reducers/index.ts b/src/app/services/reducers/index.ts index 02beeb2b20..1742e2bd0b 100644 --- a/src/app/services/reducers/index.ts +++ b/src/app/services/reducers/index.ts @@ -20,6 +20,7 @@ import snippets from '../slices/snippet.slice'; import themeChange from '../slices/theme.slice'; import termsOfUse from '../slices/terms-of-use.slice'; import sidebarProperties from '../slices/sidebar-properties.slice'; +import odataAbnf from '../slices/odataabnf.slice'; const reducers = { auth, @@ -42,7 +43,8 @@ const reducers = { sidebarProperties, snippets, termsOfUse, - theme: themeChange + theme: themeChange, + odataAbnf }; export { diff --git a/src/app/services/slices/odataabnf.slice.ts b/src/app/services/slices/odataabnf.slice.ts new file mode 100644 index 0000000000..c86964b9bd --- /dev/null +++ b/src/app/services/slices/odataabnf.slice.ts @@ -0,0 +1,40 @@ +import { createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit'; +import { ODATA_ABNF_CONSTRUCTION_RULES_ENDPOINT } from '../graph-constants'; + +const initialState = '' + +export const getRulesText = createAsyncThunk( + 'odataAbnf/getRulesText', + async (_, { rejectWithValue }) => { + try { + const response = await fetch(ODATA_ABNF_CONSTRUCTION_RULES_ENDPOINT); + if (!response.ok) { + throw new Error('Failed to fetch the OData ABNF construction rules'); + } + return await response.text(); + } catch (error) { + return rejectWithValue(ODATA_ABNF_CONSTRUCTION_RULES_ENDPOINT); + } + } +); + +const odataAbnfSlice = createSlice({ + name: 'getRules', + initialState, + reducers: { + getRules:(_state, action: PayloadAction) => { + return action.payload; + } + }, + extraReducers: (builder)=> { + builder.addCase(getRulesText.fulfilled, (_state, action)=>{ + return action.payload; + }); + builder.addCase(getRulesText.rejected, (_state, action)=>{ + return action.payload as string; + }) + } +}) + +export const {getRules} = odataAbnfSlice.actions; +export default odataAbnfSlice.reducer; From 3c3eb59d49dbe5cf608b6c3e8eb64ee554e92096 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 30 Sep 2024 15:41:39 +0300 Subject: [PATCH 2/4] Added odata abnf rules caching in local storage --- src/app/middleware/localStorageMiddleware.ts | 6 ++++++ src/app/services/graph-constants.ts | 3 ++- src/app/services/redux-constants.ts | 1 + src/index.tsx | 9 +++++++- src/modules/cache/odataAbnfRules.cache.ts | 22 ++++++++++++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/modules/cache/odataAbnfRules.cache.ts diff --git a/src/app/middleware/localStorageMiddleware.ts b/src/app/middleware/localStorageMiddleware.ts index a12673fe1a..92530aa4e4 100644 --- a/src/app/middleware/localStorageMiddleware.ts +++ b/src/app/middleware/localStorageMiddleware.ts @@ -7,9 +7,11 @@ import { CURRENT_THEME } from '../services/graph-constants'; import { getUniquePaths } from '../services/reducers/collections-reducer.util'; import { CHANGE_THEME_SUCCESS, COLLECTION_CREATE_SUCCESS, + ODATA_ABNF_RULES_CREATE_SUCCESS, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, SAMPLES_FETCH_SUCCESS } from '../services/redux-constants'; import { saveToLocalStorage } from '../utils/local-storage'; +import { odataAbnfCache } from '../../modules/cache/odataAbnfRules.cache'; const localStorageMiddleware: Middleware<{}, any, Dispatch> = () => (next) => async (value) => { const action = value as AppAction; @@ -49,6 +51,10 @@ const localStorageMiddleware: Middleware<{}, any, Dispatch> = () break; } + case ODATA_ABNF_RULES_CREATE_SUCCESS: + odataAbnfCache.saveGrammar(action.payload as string); + break; + default: break; } diff --git a/src/app/services/graph-constants.ts b/src/app/services/graph-constants.ts index 1f09c5d605..05cfac1059 100644 --- a/src/app/services/graph-constants.ts +++ b/src/app/services/graph-constants.ts @@ -34,4 +34,5 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s export const CONSENT_TYPE_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#:~:text=(eq%20only).-,consentType,-String' export const CURRENT_THEME='CURRENT_THEME'; export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas' -export const ODATA_ABNF_CONSTRUCTION_RULES_ENDPOINT = 'https://raw.githubusercontent.com/oasis-tcs/odata-abnf/refs/heads/main/abnf/odata-abnf-construction-rules.txt' \ No newline at end of file +export const ODATA_ABNF_CONSTRUCTION_RULES_ENDPOINT = 'https://raw.githubusercontent.com/oasis-tcs/odata-abnf/refs/heads/main/abnf/odata-abnf-construction-rules.txt' +export const ODATA_ABNF_RULES_OBJECT_KEY = 'odata-abnf-rules-object'; \ No newline at end of file diff --git a/src/app/services/redux-constants.ts b/src/app/services/redux-constants.ts index b0c0cb6246..7e77712f16 100644 --- a/src/app/services/redux-constants.ts +++ b/src/app/services/redux-constants.ts @@ -56,3 +56,4 @@ export const REVOKE_SCOPES_PENDING = 'auth/revokeScopes/pending'; export const REVOKE_SCOPES_SUCCESS = 'auth/revokeScopes/fulfilled'; export const REVOKE_SCOPES_ERROR = 'auth/revokeScopes/rejected'; export const COLLECTION_CREATE_SUCCESS = 'collections/createCollection'; +export const ODATA_ABNF_RULES_CREATE_SUCCESS = 'odataAbnf/getRulesText/fulfilled'; diff --git a/src/index.tsx b/src/index.tsx index db0ba7aca1..435e724174 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import App from './app/views/App'; -import { CURRENT_THEME } from './app/services/graph-constants'; +import { CURRENT_THEME, ODATA_ABNF_RULES_OBJECT_KEY } from './app/services/graph-constants'; import { getAuthTokenSuccess, getConsentedScopesSuccess } from './app/services/slices/auth.slice'; import { createCollection } from './app/services/slices/collections.slice'; import { setDevxApiUrl } from './app/services/slices/devxapi.slice'; @@ -18,6 +18,7 @@ import { fetchResources } from './app/services/slices/resources.slice'; import { setSampleQuery } from './app/services/slices/sample-query.slice'; import { toggleSidebar } from './app/services/slices/sidebar-properties.slice'; import { changeTheme } from './app/services/slices/theme.slice'; +import { getRulesText } from './app/services/slices/odataabnf.slice'; import variantService from './app/services/variant-service'; import { isValidHttpsUrl } from './app/utils/external-link-validation'; import { readFromLocalStorage } from './app/utils/local-storage'; @@ -33,6 +34,7 @@ import { IDevxAPI } from './types/devx-api'; import { Mode } from './types/enums'; import { IHistoryItem } from './types/history'; import { Collection } from './types/resources'; +import { odataAbnfCache } from './modules/cache/odataAbnfRules.cache'; const appRoot: HTMLElement = document.getElementById('root')!; @@ -98,6 +100,11 @@ const appStore: any = store; setCurrentSystemTheme(); appStore.dispatch(getGraphProxyUrl()); +const odataAbnfRules = await odataAbnfCache.readGrammar() +if (!odataAbnfRules) { + appStore.dispatch(getRulesText()) +} + function refreshAccessToken() { authenticationWrapper.getToken().then((authResponse: AuthenticationResult) => { if (authResponse && authResponse.accessToken) { diff --git a/src/modules/cache/odataAbnfRules.cache.ts b/src/modules/cache/odataAbnfRules.cache.ts new file mode 100644 index 0000000000..c08d257b8d --- /dev/null +++ b/src/modules/cache/odataAbnfRules.cache.ts @@ -0,0 +1,22 @@ +import localforage from 'localforage'; +import { ODATA_ABNF_RULES_OBJECT_KEY } from '../../app/services/graph-constants'; + +const odataAbnfStorage = localforage.createInstance({ + storeName: 'odataAbnf', + name: 'GE_V4' +}); + + +export const odataAbnfCache = (function () { + const saveGrammar = async (text: string)=>{ + await odataAbnfStorage.setItem(ODATA_ABNF_RULES_OBJECT_KEY, text) + } + + const readGrammar = async (): Promise =>{ + const rules = await odataAbnfStorage.getItem(ODATA_ABNF_RULES_OBJECT_KEY) as string; + if (rules) {return rules;} + return ''; + } + + return {saveGrammar, readGrammar} +})() \ No newline at end of file From babb2d264ec85d4739ec93f08758a098511c8220 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 30 Sep 2024 16:27:44 +0300 Subject: [PATCH 3/4] Cache the grammar object to limit running generation api.generate() takes ~700ms to run. For each url types, this can accumulate lags. Caching the object halves this time to ~280ms. --- src/index.tsx | 2 +- src/modules/cache/odataAbnfRules.cache.ts | 20 +++++++++++++++----- src/modules/validation/abnf.ts | 22 +++++++++------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 435e724174..7322d0cd04 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -101,7 +101,7 @@ setCurrentSystemTheme(); appStore.dispatch(getGraphProxyUrl()); const odataAbnfRules = await odataAbnfCache.readGrammar() -if (!odataAbnfRules) { +if (Object.keys(odataAbnfRules).length === 0) { appStore.dispatch(getRulesText()) } diff --git a/src/modules/cache/odataAbnfRules.cache.ts b/src/modules/cache/odataAbnfRules.cache.ts index c08d257b8d..2b333736ab 100644 --- a/src/modules/cache/odataAbnfRules.cache.ts +++ b/src/modules/cache/odataAbnfRules.cache.ts @@ -1,3 +1,4 @@ +import 'apg-js/dist/apg-api-bundle'; import localforage from 'localforage'; import { ODATA_ABNF_RULES_OBJECT_KEY } from '../../app/services/graph-constants'; @@ -6,16 +7,25 @@ const odataAbnfStorage = localforage.createInstance({ name: 'GE_V4' }); +const { apgApi } = globalThis as any; + export const odataAbnfCache = (function () { const saveGrammar = async (text: string)=>{ - await odataAbnfStorage.setItem(ODATA_ABNF_RULES_OBJECT_KEY, text) + const api = new apgApi(text) + api.generate() + if (api.errors.length > 0) { + throw new Error('ABNF grammar has failed to generate') + } + // Note: stringifying here loses the callbacks and toString function. They're not needed for this + // therefore this is ok. + await odataAbnfStorage.setItem(ODATA_ABNF_RULES_OBJECT_KEY, JSON.stringify(api.toObject())) } - const readGrammar = async (): Promise =>{ - const rules = await odataAbnfStorage.getItem(ODATA_ABNF_RULES_OBJECT_KEY) as string; - if (rules) {return rules;} - return ''; + const readGrammar = async (): Promise =>{ + const grammar = await odataAbnfStorage.getItem(ODATA_ABNF_RULES_OBJECT_KEY) as string; + if (grammar) {return JSON.parse(grammar);} + return {}; } return {saveGrammar, readGrammar} diff --git a/src/modules/validation/abnf.ts b/src/modules/validation/abnf.ts index 1c74b92cf8..64336dc1d6 100644 --- a/src/modules/validation/abnf.ts +++ b/src/modules/validation/abnf.ts @@ -1,5 +1,5 @@ import 'apg-js/dist/apg-api-bundle'; -import { rules } from './definition'; +import { odataAbnfCache } from '../cache/odataAbnfRules.cache'; interface ValidationResult { inputLength: number; @@ -15,36 +15,32 @@ interface ValidationResult { success: boolean; } -const { apgLib, apgApi } = globalThis as any; +const { apgLib } = globalThis as any; export class ValidatedUrl { private static grammar: any; private static parser = new apgLib.parser(); public static getGrammar() { if (!ValidatedUrl.grammar) { - ValidatedUrl.grammar = this.generateGrammarObject(); + this.generateGrammarObject().then(grammar=>{ ValidatedUrl.grammar = grammar}); } return ValidatedUrl.grammar; } - - private static generateGrammarObject() { - const api = new apgApi(rules); - api.generate(); - - if (api.errors.length) { - throw Error('ABNF grammar has errors'); - } - return api.toObject(); + private static async generateGrammarObject() { + const grammar = await odataAbnfCache.readGrammar(); + return grammar; } public validate(graphUrl: string): ValidationResult { let decodedGraphUrl = graphUrl; try { decodedGraphUrl = decodeURI(graphUrl); } catch (error) { /* empty */ } + const grammar = ValidatedUrl.getGrammar() const result = ValidatedUrl.parser.parse( - ValidatedUrl.getGrammar(), + grammar, 'odataUri', decodedGraphUrl ); + console.log(result) return result; } } \ No newline at end of file From 030c7d175fa516ade2d2f8d32678f9790e46a98a Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 30 Sep 2024 16:58:17 +0300 Subject: [PATCH 4/4] Check odata relative URI for failing or --- .../services/actions/query-action-creators.spec.ts | 8 ++++---- src/modules/validation/abnf.ts | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app/services/actions/query-action-creators.spec.ts b/src/app/services/actions/query-action-creators.spec.ts index 1641476cd9..9fd7822f2b 100644 --- a/src/app/services/actions/query-action-creators.spec.ts +++ b/src/app/services/actions/query-action-creators.spec.ts @@ -4,7 +4,7 @@ import configureMockStore from 'redux-mock-store'; import { ADD_HISTORY_ITEM_SUCCESS, QUERY_GRAPH_RUNNING, QUERY_GRAPH_STATUS, QUERY_GRAPH_SUCCESS } from '../redux-constants'; import { runQuery } from '../slices/graph-response.slice'; import { mockThunkMiddleware } from './mockThunkMiddleware'; -import { AnyAction } from '@reduxjs/toolkit'; +import { Action } from '@reduxjs/toolkit'; import { IQuery } from '../../../types/query-runner'; const mockStore = configureMockStore([mockThunkMiddleware]); @@ -68,7 +68,7 @@ describe('Query action creators', () => { selectedVersion: 'v1.0' } const store_ = mockStore({ graphResponse: '' }); - store_.dispatch(runQuery(query) as unknown as AnyAction); + store_.dispatch(runQuery(query) as unknown as Action); expect(store_.getActions().map(action => { const { meta, ...rest } = action; return rest; @@ -125,7 +125,7 @@ describe('Query action creators', () => { } const store_ = mockStore({ graphResponse: '' }); - store_.dispatch(runQuery(query) as unknown as AnyAction); + store_.dispatch(runQuery(query) as unknown as Action); const mockFetch = jest.fn().mockImplementation(() => { return Promise.resolve({ ok: false, @@ -137,7 +137,7 @@ describe('Query action creators', () => { window.fetch = mockFetch; - store_.dispatch(runQuery(query) as unknown as AnyAction) + store_.dispatch(runQuery(query) as unknown as Action) .then((response: { type: any; payload: { ok: boolean; }; }) => { expect(response.type).toBe(QUERY_GRAPH_STATUS); expect(response.payload.ok).toBe(false); diff --git a/src/modules/validation/abnf.ts b/src/modules/validation/abnf.ts index 64336dc1d6..19c38bb702 100644 --- a/src/modules/validation/abnf.ts +++ b/src/modules/validation/abnf.ts @@ -35,12 +35,20 @@ export class ValidatedUrl { let decodedGraphUrl = graphUrl; try { decodedGraphUrl = decodeURI(graphUrl); } catch (error) { /* empty */ } const grammar = ValidatedUrl.getGrammar() - const result = ValidatedUrl.parser.parse( + let result = ValidatedUrl.parser.parse( grammar, 'odataUri', decodedGraphUrl ); - console.log(result) + + if (!result.success) { + const pathname = new URL(decodedGraphUrl).pathname.replace('/v1.0/','').replace('/beta/', ''); + result = ValidatedUrl.parser.parse( + grammar, + 'odataRelativeUri', + pathname + ); + } return result; } } \ No newline at end of file